diff --git a/api-catalog-package/src/main/resources/bin/start.sh b/api-catalog-package/src/main/resources/bin/start.sh index 2df3f1581e..8681c7da3a 100755 --- a/api-catalog-package/src/main/resources/bin/start.sh +++ b/api-catalog-package/src/main/resources/bin/start.sh @@ -56,7 +56,6 @@ else fi echo "jar file: "${JAR_FILE} # script assumes it's in the catalog component directory and common_lib needs to be relative path - if [ -z "${CMMN_LB}" ] then COMMON_LIB="../apiml-common-lib/bin/api-layer-lite-lib-all.jar" @@ -64,6 +63,12 @@ else COMMON_LIB=${CMMN_LB} fi +# script assumes it's in the api-catalog component directory and jvm.security.override.properties needs to be relative path +JVM_SECURITY_PROPERTIES="" +if [ "${JVM_SECURITY_PROPERTIES_OVERRIDE:-false}" = "true" ]; then + JVM_SECURITY_PROPERTIES="-Djava.security.properties=../apiml-common-lib/bin/jvm.security.override.properties" +fi + if [ -z "${LIBRARY_PATH}" ] then LIBRARY_PATH="../common-java-lib/bin/" @@ -279,6 +284,7 @@ _BPX_JOBNAME=${ZWE_zowe_job_prefix}${CATALOG_CODE} ${JAVA_BIN_DIR}java \ ${QUICK_START} \ ${ADD_OPENS} \ ${LOGBACK} \ + ${JVM_SECURITY_PROPERTIES} \ -Dibm.serversocket.recover=true \ -Dfile.encoding=UTF-8 \ -Dlogging.charset.console=${ZOWE_CONSOLE_LOG_CHARSET} \ diff --git a/apiml-common-lib-package/build.gradle b/apiml-common-lib-package/build.gradle index 06d610e1b0..a71db1925c 100644 --- a/apiml-common-lib-package/build.gradle +++ b/apiml-common-lib-package/build.gradle @@ -29,6 +29,7 @@ task packageCommonLib(type: Zip) { into('bin/') { from "${project.rootDir}/build/libs/api-layer-lite-lib-all" + ".jar" + from "$resourceDir/jvm.security.override.properties" } } diff --git a/apiml-common-lib-package/src/main/resources/jvm.security.override.properties b/apiml-common-lib-package/src/main/resources/jvm.security.override.properties new file mode 100644 index 0000000000..cb1a324873 --- /dev/null +++ b/apiml-common-lib-package/src/main/resources/jvm.security.override.properties @@ -0,0 +1,16 @@ +security.provider.1=IBMJCEHYBRID +security.provider.2=IBMJCECCA +security.provider.3=IBMZSecurity +security.provider.4=OpenJCEPlus +security.provider.5=SUN +security.provider.6=SunRsaSign +security.provider.7=SunEC +security.provider.8=SunJSSE +security.provider.9=SunJCE +security.provider.10=SunJGSS +security.provider.11=SunSASL +security.provider.12=XMLDSig +security.provider.13=SunPCSC +security.provider.14=JdkLDAP +security.provider.15=JdkSASL +security.provider.16=SunPKCS11 diff --git a/apiml-package/src/main/resources/bin/start.sh b/apiml-package/src/main/resources/bin/start.sh index 0d241bcb2b..96467ee7c0 100755 --- a/apiml-package/src/main/resources/bin/start.sh +++ b/apiml-package/src/main/resources/bin/start.sh @@ -128,10 +128,24 @@ else COMMON_LIB="${CMMN_LB}" fi +# script assumes it's in the apiml component directory and jvm.security.override.properties needs to be relative path +JVM_SECURITY_PROPERTIES="" +if [ "${JVM_SECURITY_PROPERTIES_OVERRIDE:-false}" = "true" ]; then + JVM_SECURITY_PROPERTIES="-Djava.security.properties=../apiml-common-lib/bin/jvm.security.override.properties" +fi + if [ -z "${LIBRARY_PATH}" ]; then LIBRARY_PATH="../common-java-lib/bin/" fi +add_profile() { + new_profile=$1 + if [ -n "${ZWE_configs_spring_profiles_active}" ]; then + ZWE_configs_spring_profiles_active="${ZWE_configs_spring_profiles_active}," + fi + ZWE_configs_spring_profiles_active="${ZWE_configs_spring_profiles_active}${new_profile}" +} + if [ "${ZWE_components_gateway_debug:-${ZWE_configs_debug:-false}}" = "true" ]; then # TODO should this be a merge of the profiles in gateway and discovery (and other modules later added?) if [ -n "${ZWE_configs_spring_profiles_active:-${ZWE_components_gateway_spring_profiles_active:-${ZWE_components_discovery_spring_profiles_active}}}" ]; then @@ -329,6 +343,7 @@ _BPX_JOBNAME=${ZWE_zowe_job_prefix}${APIML_CODE} ${JAVA_BIN_DIR}java \ ${QUICK_START} \ ${ADD_OPENS} \ ${LOGBACK} \ + ${JVM_SECURITY_PROPERTIES} \ -Dapiml.cache.storage.location=${ZWE_zowe_workspaceDirectory}/api-mediation/${ZWE_haInstance_id:-localhost} \ -Dapiml.catalog.customStyle.backgroundColor=${ZWE_components_apicatalog_apiml_catalog_customStyle_backgroundColor:-${ZWE_configs_apiml_catalog_customStyle_backgroundColor:-}} \ -Dapiml.catalog.customStyle.docLink=${ZWE_components_apicatalog_apiml_catalog_customStyle_docLink:-${ZWE_configs_apiml_catalog_customStyle_docLink:-}} \ @@ -401,7 +416,7 @@ _BPX_JOBNAME=${ZWE_zowe_job_prefix}${APIML_CODE} ${JAVA_BIN_DIR}java \ -Dapiml.service.allowEncodedSlashes=${ZWE_components_gateway_apiml_service_allowEncodedSlashes:-${ZWE_configs_apiml_service_allowEncodedSlashes:-true}} \ -Dapiml.service.apimlId=${ZWE_components_gateway_apimlId:-${ZWE_configs_apimlId:-}} \ -Dapiml.service.corsEnabled=${ZWE_components_gateway_apiml_service_corsEnabled:-${ZWE_configs_apiml_service_corsEnabled:-false}} \ - -Dapiml.service.corsAllowedMethods=${ZWE_components_gateway_apiml_service_corsAllowedMethods:-${ZWE_configs_apiml_service_corsAllowedMethods:-}} \ + -Dapiml.service.corsAllowedMethods=${ZWE_components_gateway_apiml_service_corsAllowedMethods:-${ZWE_configs_apiml_service_corsAllowedMethods:-GET,HEAD,POST,PATCH,DELETE,PUT,OPTIONS}} \ -Dapiml.service.externalUrl="${externalProtocol}://${ZWE_zowe_externalDomains_0}:${ZWE_zowe_externalPort}" \ -Dapiml.service.forwardClientCertEnabled=${ZWE_components_gateway_apiml_security_x509_enabled:-${ZWE_configs_apiml_security_x509_enabled:-false}} \ -Dapiml.service.hostname=${ZWE_haInstance_hostname:-localhost} \ diff --git a/apiml-security-common/build.gradle b/apiml-security-common/build.gradle index 0efdd1923d..a123c05ed3 100644 --- a/apiml-security-common/build.gradle +++ b/apiml-security-common/build.gradle @@ -1,3 +1,7 @@ +plugins { + id "java-test-fixtures" +} + dependencies { api project(':apiml-common') @@ -12,6 +16,8 @@ dependencies { implementation libs.apache.commons.lang3 implementation libs.http.client5 + implementation libs.nimbus.jose.jwt // Parsing + testImplementation libs.spring.boot.starter.test testImplementation(testFixtures(project(":apiml-common"))) @@ -21,4 +27,13 @@ dependencies { testCompileOnly libs.lombok testImplementation libs.netty.reactor.http testAnnotationProcessor libs.lombok + + testFixturesImplementation libs.nimbus.jose.jwt + testFixturesImplementation libs.jose4j.jwt + testFixturesImplementation libs.jjwt + testFixturesImplementation libs.jjwt.impl + testFixturesImplementation libs.jjwt.jackson + + testFixturesCompileOnly libs.lombok + testFixturesAnnotationProcessor libs.lombok } diff --git a/zaas-service/src/main/java/org/zowe/apiml/zaas/security/service/JwtUtils.java b/apiml-security-common/src/main/java/org/zowe/apiml/security/common/util/JwtUtils.java similarity index 70% rename from zaas-service/src/main/java/org/zowe/apiml/zaas/security/service/JwtUtils.java rename to apiml-security-common/src/main/java/org/zowe/apiml/security/common/util/JwtUtils.java index 35e4908b38..9338f2a097 100644 --- a/zaas-service/src/main/java/org/zowe/apiml/zaas/security/service/JwtUtils.java +++ b/apiml-security-common/src/main/java/org/zowe/apiml/security/common/util/JwtUtils.java @@ -8,13 +8,12 @@ * Copyright Contributors to the Zowe Project. */ -package org.zowe.apiml.zaas.security.service; +package org.zowe.apiml.security.common.util; -import io.jsonwebtoken.Claims; -import io.jsonwebtoken.ExpiredJwtException; -import io.jsonwebtoken.JwtException; -import io.jsonwebtoken.Jwts; -import io.jsonwebtoken.MalformedJwtException; +import com.nimbusds.jwt.JWTClaimsSet; +import com.nimbusds.jwt.JWTParser; +import com.nimbusds.jwt.proc.BadJWTException; +import com.nimbusds.jwt.proc.ExpiredJWTException; import lombok.experimental.UtilityClass; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; @@ -23,6 +22,8 @@ import org.zowe.apiml.security.common.token.TokenNotValidException; import java.nio.charset.StandardCharsets; +import java.text.ParseException; +import java.time.Instant; import java.util.Base64; import java.util.List; import java.util.Map; @@ -43,17 +44,21 @@ public class JwtUtils { * @return parsed claims * @throws TokenNotValidException in case of invalid input, or TokenExpireException if JWT is expired */ - public static Claims getJwtClaims(String jwt) { + public JWTClaimsSet getJwtClaims(String jwt) { /* * Removes signature, because we don't have key to verify z/OS tokens, and we just need to read claim. * Verification is done by SAF itself. JWT library doesn't parse signed key without verification. */ try { - String withoutSign = removeJwtSign(jwt); - return Jwts.parser().unsecured().build() - .parseUnsecuredClaims(withoutSign) - .getPayload(); - } catch (RuntimeException exception) { + String jwtWithoutSignature = removeJwtSign(jwt); + var token = JWTParser.parse(jwtWithoutSignature); + var claims = token.getJWTClaimsSet(); + + if (claims.getExpirationTime().toInstant().isBefore(Instant.now())) { + throw new ExpiredJWTException("JWT token is expired"); + } + return token.getJWTClaimsSet(); + } catch (RuntimeException | ParseException | BadJWTException exception) { throw handleJwtParserException(exception); } } @@ -64,13 +69,16 @@ public static Claims getJwtClaims(String jwt) { * * @param jwtToken token to modify * @return unsigned jwt token + * @throws BadJWTException */ - public static String removeJwtSign(String jwtToken) { + public String removeJwtSign(String jwtToken) throws BadJWTException { if (jwtToken == null) return null; int firstDot = jwtToken.indexOf('.'); int lastDot = jwtToken.lastIndexOf('.'); - if ((firstDot < 0) || (firstDot >= lastDot)) throw new MalformedJwtException("Invalid JWT format"); + if ((firstDot < 0) || (firstDot >= lastDot)) { + throw new BadJWTException("Invalid JWT format"); + } return HEADER_NONE_SIGNATURE + jwtToken.substring(firstDot, lastDot + 1); } @@ -81,12 +89,12 @@ public static String removeJwtSign(String jwtToken) { * @param exception original exception * @return translated exception (better messaging and allow subsequent handling) */ - public static RuntimeException handleJwtParserException(RuntimeException exception) { - if (exception instanceof ExpiredJwtException expiredJwtException) { - log.debug("Token with id '{}' for user '{}' is expired.", expiredJwtException.getClaims().getId(), expiredJwtException.getClaims().getSubject()); + public RuntimeException handleJwtParserException(Exception exception) { + if (exception instanceof ExpiredJWTException) { + log.debug("Token is expired."); return new TokenExpireException("Token is expired.", exception); } - if (exception instanceof JwtException) { + if (exception instanceof BadJWTException || exception instanceof ParseException) { log.debug(TOKEN_IS_NOT_VALID_DUE_TO, exception.getMessage()); return new TokenNotValidException("Token is not valid.", exception); } @@ -95,6 +103,10 @@ public static RuntimeException handleJwtParserException(RuntimeException excepti return new TokenNotValidException("An internal error occurred while validating the token therefore the token is no longer valid.", exception); } + boolean verifyJwtSignatureWithJwk() { + return false; + } + /** * Extracts value of a field from an OIDC token. The value is extracted from a custom path which supports nested objects. * @param token to extract the field from @@ -103,18 +115,18 @@ public static RuntimeException handleJwtParserException(RuntimeException excepti * * @throws TokenFormatNotValidException in case of the field value cannot be extracted from the token, is null, or empty */ - public static List getFieldValuesFromToken(String token, List pathToField) throws TokenFormatNotValidException { + public List getFieldValuesFromToken(String token, List pathToField) throws TokenFormatNotValidException { if (token == null || pathToField == null || pathToField.isEmpty() || StringUtils.isBlank(pathToField.get(0))) { throw new IllegalArgumentException("Token and field path must not be null or empty"); } try { - Claims claims = getJwtClaims(token); + var claims = getJwtClaims(token); List fieldValues; if (pathToField.size() == 1) { fieldValues = extractHighLevelField(claims, pathToField); } else { - fieldValues = extractNestedFields(claims, pathToField); + fieldValues = extractNestedFields(claims, pathToField); } fieldValues = fieldValues.stream().filter(StringUtils::isNotBlank).toList(); @@ -129,23 +141,24 @@ public static List getFieldValuesFromToken(String token, List pa } } - private List extractHighLevelField(Claims claims, List pathToField) { - return extractValueAsList(claims.get(pathToField.get(0))); + private List extractHighLevelField(JWTClaimsSet claims, List pathToField) { + return extractValueAsList(claims.getClaim(pathToField.get(0))); } - @SuppressWarnings({ "unchecked", "rawtypes" }) - private List extractNestedFields(Claims claims, List pathToField) { + @SuppressWarnings({ "rawtypes" }) + private List extractNestedFields(JWTClaimsSet claims, List pathToField) { var iterator = pathToField.iterator(); var key = iterator.next(); - Map val = claims.get(key, Map.class); + + var claim = claims.getClaim(key); while (iterator.hasNext()) { key = iterator.next(); - if (iterator.hasNext()) { - val = (Map) val.get(key); + if (iterator.hasNext() && claim instanceof Map val) { + claim = val.get(key); } } - return extractValueAsList(val.get(key)); + return extractValueAsList(((Map) claim).get(key)); } @SuppressWarnings("unchecked") @@ -157,6 +170,7 @@ private List extractValueAsList(Object rawValue) { } else { throw new IllegalArgumentException("Field value is neither String nor List of Strings"); } + } } diff --git a/zaas-service/src/test/java/org/zowe/apiml/zaas/security/service/JwtUtilsTest.java b/apiml-security-common/src/test/java/org/zowe/apiml/security/common/util/JwtUtilsTest.java similarity index 86% rename from zaas-service/src/test/java/org/zowe/apiml/zaas/security/service/JwtUtilsTest.java rename to apiml-security-common/src/test/java/org/zowe/apiml/security/common/util/JwtUtilsTest.java index 925916bdc8..02fa85de41 100644 --- a/zaas-service/src/test/java/org/zowe/apiml/zaas/security/service/JwtUtilsTest.java +++ b/apiml-security-common/src/test/java/org/zowe/apiml/security/common/util/JwtUtilsTest.java @@ -8,12 +8,10 @@ * Copyright Contributors to the Zowe Project. */ -package org.zowe.apiml.zaas.security.service; +package org.zowe.apiml.security.common.util; -import io.jsonwebtoken.Claims; -import io.jsonwebtoken.ExpiredJwtException; -import io.jsonwebtoken.Header; -import io.jsonwebtoken.JwtException; +import com.nimbusds.jwt.proc.BadJWTException; +import com.nimbusds.jwt.proc.ExpiredJWTException; import org.hamcrest.Matchers; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; @@ -29,10 +27,9 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.mockito.Mockito.mock; -import static org.zowe.apiml.zaas.utils.JWTUtils.createTokenWithUserFields; +import static org.zowe.apiml.security.common.util.JWTTestUtils.createTokenWithUserFields; class JwtUtilsTest { @@ -40,24 +37,22 @@ class JwtUtilsTest { @Test void testHandleJwtParserExceptionForExpiredToken() { - - Exception exception = JwtUtils.handleJwtParserException(new ExpiredJwtException(mock(Header.class), mock(Claims.class), "msg")); - assertTrue(exception instanceof TokenExpireException); + Exception exception = JwtUtils.handleJwtParserException(new ExpiredJWTException("msg")); + assertInstanceOf(TokenExpireException.class, exception); assertEquals("Token is expired.", exception.getMessage()); } @Test void testHandleJwtParserExceptionForInvalidToken() { - - Exception exception = JwtUtils.handleJwtParserException(new JwtException("msg")); - assertTrue(exception instanceof TokenNotValidException); + Exception exception = JwtUtils.handleJwtParserException(new BadJWTException("msg")); + assertInstanceOf(TokenNotValidException.class, exception); assertEquals("Token is not valid.", exception.getMessage()); } @Test void testHandleJwtParserRuntimeException() { Exception exception = JwtUtils.handleJwtParserException(new RuntimeException("msg")); - assertTrue(exception instanceof TokenNotValidException); + assertInstanceOf(TokenNotValidException.class, exception); assertEquals("An internal error occurred while validating the token therefore the token is no longer valid.", exception.getMessage()); } diff --git a/zaas-service/src/test/java/org/zowe/apiml/zaas/utils/JWTUtils.java b/apiml-security-common/src/testFixtures/java/org/zowe/apiml/security/common/util/JWTTestUtils.java similarity index 86% rename from zaas-service/src/test/java/org/zowe/apiml/zaas/utils/JWTUtils.java rename to apiml-security-common/src/testFixtures/java/org/zowe/apiml/security/common/util/JWTTestUtils.java index e0114f3a55..c30fca1605 100644 --- a/zaas-service/src/test/java/org/zowe/apiml/zaas/utils/JWTUtils.java +++ b/apiml-security-common/src/testFixtures/java/org/zowe/apiml/security/common/util/JWTTestUtils.java @@ -8,12 +8,12 @@ * Copyright Contributors to the Zowe Project. */ -package org.zowe.apiml.zaas.utils; +package org.zowe.apiml.security.common.util; -import com.nimbusds.jose.jwk.JWKSet; -import com.nimbusds.jose.jwk.RSAKey; import io.jsonwebtoken.Jwts; import lombok.SneakyThrows; +import org.jose4j.jwk.JsonWebKey; +import org.jose4j.jwk.JsonWebKeySet; import org.zowe.apiml.security.HttpsConfig; import org.zowe.apiml.security.SecurityUtils; @@ -28,7 +28,7 @@ import java.util.Map; import java.util.UUID; -public class JWTUtils { +public class JWTTestUtils { public static String createZoweJwtToken(String username, String domain, String ltpaToken, HttpsConfig config) { return createToken(username, domain, ltpaToken, config, "APIML"); @@ -42,6 +42,7 @@ public static String createToken(String username, String domain, String ltpaToke long now = System.currentTimeMillis(); long expiration = now + 100_000L; Key jwtSecret = SecurityUtils.loadKey(config); + return Jwts.builder() .subject(username) .claim("dom", domain) @@ -59,7 +60,7 @@ public static String createTokenWithUserFields() { var now = Instant.now(); var jwkAndSet = loadPrivateKey("../keystore/localhost/localhost.keystore.p12", "localhost", "password"); return Jwts.builder() - .header().keyId("0987").and() + .header().keyId("Lcxckkor94qkrunxHP7Tkib547rzmkXvsYV-nc6U-N4").and() .subject("oidc.username") .claim("email", "username@oidc.org") .claim("nullValue", null) @@ -90,13 +91,14 @@ public static JwkAndSet loadPrivateKey(String path, String alias, String passwor var cert = ks.getCertificate(alias); var pubKey = cert.getPublicKey(); if (pubKey instanceof RSAPublicKey rsaPublicKey) { - var k = new RSAKey.Builder(rsaPublicKey).keyID("0987").build().toPublicJWK(); - return new JwkAndSet((PrivateKey) key, new JWKSet(k)); + var jwk = JsonWebKey.Factory.newJwk(rsaPublicKey); + jwk.setKeyId("Lcxckkor94qkrunxHP7Tkib547rzmkXvsYV-nc6U-N4"); + return new JwkAndSet((PrivateKey) key, new JsonWebKeySet(jwk)); } return new JwkAndSet((PrivateKey) key, null); } - public record JwkAndSet(PrivateKey privateKey, JWKSet jwkSet) { + public record JwkAndSet(PrivateKey privateKey, JsonWebKeySet jwkSet) { } } diff --git a/apiml/build.gradle b/apiml/build.gradle index 10bf995687..61d8e4497a 100644 --- a/apiml/build.gradle +++ b/apiml/build.gradle @@ -73,6 +73,7 @@ dependencies { implementation libs.spring.boot.starter.security implementation libs.nimbus.jose.jwt implementation libs.spring.doc.webflux.ui + implementation libs.jose4j.jwt // Signing, with support for JCA with ICSF testImplementation(testFixtures(project(":apiml-common"))) testImplementation(testFixtures(project(":gateway-service"))) diff --git a/apiml/src/main/java/org/zowe/apiml/controller/ReactivePublicJWKController.java b/apiml/src/main/java/org/zowe/apiml/controller/ReactivePublicJWKController.java index 73a0339235..5aea10820b 100644 --- a/apiml/src/main/java/org/zowe/apiml/controller/ReactivePublicJWKController.java +++ b/apiml/src/main/java/org/zowe/apiml/controller/ReactivePublicJWKController.java @@ -10,10 +10,7 @@ package org.zowe.apiml.controller; -import com.nimbusds.jose.JOSEException; -import com.nimbusds.jose.jwk.JWK; import com.nimbusds.jose.jwk.JWKSet; -import com.nimbusds.jose.jwk.RSAKey; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.Schema; @@ -23,6 +20,10 @@ import lombok.extern.slf4j.Slf4j; import org.bouncycastle.util.io.pem.PemObject; import org.bouncycastle.util.io.pem.PemWriter; +import org.jose4j.jwk.JsonWebKey; +import org.jose4j.jwk.JsonWebKeySet; +import org.jose4j.jwk.RsaJsonWebKey; +import org.jose4j.lang.JoseException; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; @@ -43,8 +44,6 @@ import java.util.Collections; import java.util.LinkedList; import java.util.List; -import java.util.Map; -import java.util.Optional; import static org.zowe.apiml.zaas.controllers.AuthController.ALL_PUBLIC_KEYS_PATH; import static org.zowe.apiml.zaas.controllers.AuthController.CURRENT_PUBLIC_KEYS_PATH; @@ -81,23 +80,23 @@ public class ReactivePublicJWKController { ) ) }) - public Mono> getAllPublicKeys() { + public Mono> getAllPublicKeys() { return Mono.fromSupplier(() -> { - List keys; + List keys; if (jwtSecurity.actualJwtProducer() == JwtSecurity.JwtProducer.ZOSMF) { - keys = new LinkedList<>(zosmfService.getPublicKeys().getKeys()); + keys = new LinkedList<>(zosmfService.getPublicKeys().getJsonWebKeys()); } else { keys = new LinkedList<>(); } - Optional key = jwtSecurity.getJwkPublicKey(); + var key = jwtSecurity.getJwkPublicKey(); key.ifPresent(keys::add); if ((oidcProvider != null) && (oidcProvider instanceof OIDCTokenProvider oidcTokenProvider)) { - JWKSet oidcSet = oidcTokenProvider.getJwkSet(); + var oidcSet = oidcTokenProvider.getJwkSet(); if (oidcSet != null) { - keys.addAll(oidcSet.getKeys()); + keys.addAll(oidcSet.getJsonWebKeys()); } } - return new JWKSet(keys).toJSONObject(true); + return ResponseEntity.ok(new JsonWebKeySet(keys).toJson()); }); } @@ -121,10 +120,10 @@ public Mono> getAllPublicKeys() { ) ) }) - public Mono> getCurrentPublicKeys() { + public Mono> getCurrentPublicKeys() { return Mono.fromSupplier(() -> { - final List keys = getCurrentKey(); - return new JWKSet(keys).toJSONObject(true); + var keys = getCurrentKey(); + return ResponseEntity.ok(new JsonWebKeySet(keys).toJson()); }); } @@ -152,9 +151,9 @@ public Mono> getCurrentPublicKeys() { }) public Mono> getPublicKeyUsedForSigning() { return Mono.fromSupplier(() -> { - List publicKeys = getCurrentKey().stream() - .filter(RSAKey.class::isInstance) - .toList(); + var publicKeys = getCurrentKey().stream() + .filter(RsaJsonWebKey.class::isInstance) + .toList(); if (publicKeys.isEmpty()) { log.debug("JWT setup was not yet initialized so there is no public key for response."); return new ResponseEntity<>(messageService.createMessage("org.zowe.apiml.zaas.keys.unknownState").mapToApiMessage(), HttpStatus.INTERNAL_SERVER_ERROR); @@ -164,11 +163,10 @@ public Mono> getPublicKeyUsedForSigning() { return new ResponseEntity<>(messageService.createMessage("org.zowe.apiml.zaas.keys.wrongAmount", publicKeys.size()).mapToApiMessage(), HttpStatus.INTERNAL_SERVER_ERROR); } try { - PublicKey key = publicKeys.get(0) - .toRSAKey() - .toPublicKey(); + RsaJsonWebKey jwk = (RsaJsonWebKey) JsonWebKey.Factory.newJwk(publicKeys.get(0).toJson()); + PublicKey key = jwk.getPublicKey(); return new ResponseEntity<>(getPublicKeyAsPem(key), HttpStatus.OK); - } catch (IOException | JOSEException ex) { + } catch (IOException | JoseException ex) { log.error("It was not possible to get public key for JWK, exception message: {}", ex.getMessage()); return new ResponseEntity<>(messageService.createMessage("org.zowe.apiml.zaas.keys.unknown").mapToApiMessage(), HttpStatus.INTERNAL_SERVER_ERROR); } @@ -184,10 +182,10 @@ private String getPublicKeyAsPem(PublicKey publicKey) throws IOException { return stringWriter.toString(); } - private List getCurrentKey() { + private List getCurrentKey() { JwtSecurity.JwtProducer producer = jwtSecurity.actualJwtProducer(); - JWKSet currentKey; + JsonWebKeySet currentKey; switch (producer) { case ZOSMF: currentKey = zosmfService.getPublicKeys(); @@ -199,7 +197,7 @@ private List getCurrentKey() { //return 500 as we just don't know yet. return Collections.emptyList(); } - return currentKey.getKeys(); + return currentKey.getJsonWebKeys(); } } diff --git a/apiml/src/test/java/org/zowe/apiml/acceptance/AcceptanceTest.java b/apiml/src/test/java/org/zowe/apiml/acceptance/AcceptanceTest.java index c44f5db764..fd31b07419 100644 --- a/apiml/src/test/java/org/zowe/apiml/acceptance/AcceptanceTest.java +++ b/apiml/src/test/java/org/zowe/apiml/acceptance/AcceptanceTest.java @@ -10,6 +10,8 @@ package org.zowe.apiml.acceptance; +import org.junit.jupiter.api.parallel.Execution; +import org.junit.jupiter.api.parallel.ExecutionMode; import org.springframework.boot.test.autoconfigure.web.reactive.AutoConfigureWebTestClient; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.cloud.netflix.eureka.server.EurekaController; @@ -55,6 +57,7 @@ "server.port=40985" // Use specific port due to need to use of apiml.service.port to determine if it's gateway or DS } ) +@Execution(ExecutionMode.SAME_THREAD) @ActiveProfiles("ApimlModulithAcceptanceTest") @AutoConfigureWebTestClient @DirtiesContext diff --git a/apiml/src/test/java/org/zowe/apiml/controller/ReactivePublicJWKControllerTest.java b/apiml/src/test/java/org/zowe/apiml/controller/ReactivePublicJWKControllerTest.java index bac2233d84..b784876a06 100644 --- a/apiml/src/test/java/org/zowe/apiml/controller/ReactivePublicJWKControllerTest.java +++ b/apiml/src/test/java/org/zowe/apiml/controller/ReactivePublicJWKControllerTest.java @@ -10,10 +10,11 @@ package org.zowe.apiml.controller; -import com.nimbusds.jose.JOSEException; -import com.nimbusds.jose.jwk.JWK; -import com.nimbusds.jose.jwk.JWKSet; -import com.nimbusds.jose.jwk.RSAKey; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.jose4j.jwk.JsonWebKey; +import org.jose4j.jwk.JsonWebKeySet; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; @@ -32,23 +33,20 @@ import reactor.core.publisher.Mono; import reactor.test.StepVerifier; +import java.math.BigInteger; import java.security.KeyPair; import java.security.KeyPairGenerator; import java.security.NoSuchAlgorithmException; import java.security.interfaces.RSAPublicKey; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Optional; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.mockito.Mockito.doThrow; -import static org.mockito.Mockito.lenient; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; +import static org.junit.jupiter.api.Assertions.fail; +import static org.mockito.Mockito.*; @ExtendWith(MockitoExtension.class) class ReactivePublicJWKControllerTest { @@ -61,49 +59,73 @@ class ReactivePublicJWKControllerTest { @InjectMocks private ReactivePublicJWKController controller; + private ObjectMapper mapper = new ObjectMapper(); + @Test void getAllPublicKeys_zosmfProducer_withOidc() throws Exception { - JWK zosmfJwk = new RSAKey.Builder((RSAPublicKey) generateKeyPair().getPublic()).keyID("zosmfKey").build(); - JWKSet zosmfKeySet = new JWKSet(zosmfJwk); - JWK apimlJwk = new RSAKey.Builder((RSAPublicKey) generateKeyPair().getPublic()).keyID("apimlKey").build(); - JWK oidcJwk = new RSAKey.Builder((RSAPublicKey) generateKeyPair().getPublic()).keyID("oidcKey").build(); - JWKSet oidcKeySet = new JWKSet(oidcJwk); + var zosmfJwk = JsonWebKey.Factory.newJwk(generateKeyPair().getPublic()); + zosmfJwk.setKeyId("zosmfKey"); + var zosmfKeySet = new JsonWebKeySet(zosmfJwk); + var apimlJwk = JsonWebKey.Factory.newJwk(generateKeyPair().getPublic()); + apimlJwk.setKeyId("apimlKey"); + var oidcJwk = JsonWebKey.Factory.newJwk(generateKeyPair().getPublic()); + oidcJwk.setKeyId("oidcKey"); + var oidcKeySet = new JsonWebKeySet(oidcJwk); OIDCTokenProvider mockOidcProviderJwk = mock(OIDCTokenProvider.class); when(jwtSecurity.actualJwtProducer()).thenReturn(JwtSecurity.JwtProducer.ZOSMF); + + + new JsonWebKeySet(); + when(zosmfService.getPublicKeys()).thenReturn(zosmfKeySet); when(jwtSecurity.getJwkPublicKey()).thenReturn(Optional.of(apimlJwk)); var testControllerWithOidc = new ReactivePublicJWKController(mockOidcProviderJwk, jwtSecurity, zosmfService, messageService); when(mockOidcProviderJwk.getJwkSet()).thenReturn(oidcKeySet); - Mono> result = testControllerWithOidc.getAllPublicKeys(); + var result = testControllerWithOidc.getAllPublicKeys(); StepVerifier.create(result) - .expectNextMatches(jsonObject -> { - List> keys = (List>) jsonObject.get("keys"); - assertEquals(3, keys.size()); // zosmf, apiml, oidc - return keys.stream().anyMatch(k -> "zosmfKey".equals(k.get("kid"))) && - keys.stream().anyMatch(k -> "apimlKey".equals(k.get("kid"))) && - keys.stream().anyMatch(k -> "oidcKey".equals(k.get("kid"))); + .expectNextMatches(responseEntity -> { + HashMap jsonObject; + try { + jsonObject = mapper.readValue(responseEntity.getBody(), new TypeReference>() {}); + List> keys = (List>) jsonObject.get("keys"); + assertEquals(3, keys.size()); // zosmf, apiml, oidc + return keys.stream().anyMatch(k -> "zosmfKey".equals(k.get("kid"))) && + keys.stream().anyMatch(k -> "apimlKey".equals(k.get("kid"))) && + keys.stream().anyMatch(k -> "oidcKey".equals(k.get("kid"))); + } catch (JsonProcessingException e) { + fail(e); + return false; + } }) .verifyComplete(); } @Test void getAllPublicKeys_apimlProducer_noOidc() throws Exception { - var apimlJwk = new RSAKey.Builder((RSAPublicKey) generateKeyPair().getPublic()).keyID("apimlKey").build(); + var apimlJwk = JsonWebKey.Factory.newJwk(generateKeyPair().getPublic()); + apimlJwk.setKeyId("apimlKey"); var testControllerNoOidc = new ReactivePublicJWKController(null, jwtSecurity, zosmfService, messageService); when(jwtSecurity.actualJwtProducer()).thenReturn(JwtSecurity.JwtProducer.APIML); when(jwtSecurity.getJwkPublicKey()).thenReturn(Optional.of(apimlJwk)); - Mono> result = testControllerNoOidc.getAllPublicKeys(); + var result = testControllerNoOidc.getAllPublicKeys(); StepVerifier.create(result) - .expectNextMatches(jsonObject -> { + .expectNextMatches(responseEntity -> { + HashMap jsonObject; + try { + jsonObject = mapper.readValue(responseEntity.getBody(), new TypeReference>() {}); + } catch (JsonProcessingException e) { + fail(e); + return false; + } List> keys = (List>) jsonObject.get("keys"); assertEquals(1, keys.size()); return "apimlKey".equals(keys.get(0).get("kid")); @@ -115,16 +137,24 @@ void getAllPublicKeys_apimlProducer_noOidc() throws Exception { @Test void getCurrentPublicKeys_apimlProducer() throws Exception { - JWK apimlJwk = new RSAKey.Builder((RSAPublicKey) generateKeyPair().getPublic()).keyID("currentApimlKey").build(); - JWKSet apimlKeySet = new JWKSet(apimlJwk); + var apimlJwk = JsonWebKey.Factory.newJwk(generateKeyPair().getPublic()); + apimlJwk.setKeyId("currentApimlKey"); + var apimlKeySet = new JsonWebKeySet(apimlJwk); when(jwtSecurity.actualJwtProducer()).thenReturn(JwtSecurity.JwtProducer.APIML); when(jwtSecurity.getPublicKeyInSet()).thenReturn(apimlKeySet); - Mono> result = controller.getCurrentPublicKeys(); + var result = controller.getCurrentPublicKeys(); StepVerifier.create(result) - .expectNextMatches(jsonObject -> { + .expectNextMatches(responseEntity -> { + HashMap jsonObject; + try { + jsonObject = mapper.readValue(responseEntity.getBody(), new TypeReference>() {}); + } catch (JsonProcessingException e) { + fail(e); + return false; + } List> keys = (List>) jsonObject.get("keys"); assertEquals(1, keys.size()); return "currentApimlKey".equals(keys.get(0).get("kid")); @@ -134,16 +164,24 @@ void getCurrentPublicKeys_apimlProducer() throws Exception { @Test void getCurrentPublicKeys_zosmfProducer() throws Exception { - JWK zosmfJwk = new RSAKey.Builder((RSAPublicKey) generateKeyPair().getPublic()).keyID("currentZosmfKey").build(); - JWKSet zosmfKeySet = new JWKSet(zosmfJwk); + var zosmfJwk = JsonWebKey.Factory.newJwk(generateKeyPair().getPublic()); + zosmfJwk.setKeyId("currentZosmfKey"); + var zosmfKeySet = new JsonWebKeySet(zosmfJwk); when(jwtSecurity.actualJwtProducer()).thenReturn(JwtSecurity.JwtProducer.ZOSMF); when(zosmfService.getPublicKeys()).thenReturn(zosmfKeySet); - Mono> result = controller.getCurrentPublicKeys(); + var result = controller.getCurrentPublicKeys(); StepVerifier.create(result) - .expectNextMatches(jsonObject -> { + .expectNextMatches(responseEntity -> { + HashMap jsonObject; + try { + jsonObject = mapper.readValue(responseEntity.getBody(), new TypeReference>() {}); + } catch (JsonProcessingException e) { + fail(e); + return false; + } List> keys = (List>) jsonObject.get("keys"); assertEquals(1, keys.size()); return "currentZosmfKey".equals(keys.get(0).get("kid")); @@ -155,10 +193,17 @@ void getCurrentPublicKeys_zosmfProducer() throws Exception { void getCurrentPublicKeys_unknownProducer() { when(jwtSecurity.actualJwtProducer()).thenReturn(JwtSecurity.JwtProducer.UNKNOWN); // Or any other not APIML/ZOSMF - Mono> result = controller.getCurrentPublicKeys(); + var result = controller.getCurrentPublicKeys(); StepVerifier.create(result) - .expectNextMatches(jsonObject -> { + .expectNextMatches(responseEntity -> { + HashMap jsonObject; + try { + jsonObject = mapper.readValue(responseEntity.getBody(), new TypeReference>() {}); + } catch (JsonProcessingException e) { + fail(e); + return false; + } List> keys = (List>) jsonObject.get("keys"); return keys.isEmpty(); }) @@ -168,9 +213,10 @@ void getCurrentPublicKeys_unknownProducer() { @Test void getPublicKeyUsedForSigning_success() throws Exception { - KeyPair keyPair = generateKeyPair(); - JWK jwk = new RSAKey.Builder((RSAPublicKey) keyPair.getPublic()).keyID("signingKey").build(); - JWKSet keySet = new JWKSet(jwk); + var keyPair = generateKeyPair(); + var jwk = JsonWebKey.Factory.newJwk(keyPair.getPublic()); + jwk.setKeyId("signingKey"); + var keySet = new JsonWebKeySet(jwk); when(jwtSecurity.actualJwtProducer()).thenReturn(JwtSecurity.JwtProducer.APIML); when(jwtSecurity.getPublicKeyInSet()).thenReturn(keySet); @@ -202,11 +248,15 @@ void getPublicKeyUsedForSigning_noKeyAvailable() { @Test void givenMultipleKeys_thenReturnErrorWithCorrectMessage() throws Exception { - KeyPair kp1 = generateKeyPair(); - KeyPair kp2 = generateKeyPair(); - JWK jwk1 = new RSAKey.Builder((RSAPublicKey) kp1.getPublic()).keyID("key1").build(); - JWK jwk2 = new RSAKey.Builder((RSAPublicKey) kp2.getPublic()).keyID("key2").build(); - JWKSet keySet = new JWKSet(List.of(jwk1, jwk2)); + var kp1 = generateKeyPair(); + var kp2 = generateKeyPair(); + + var jwk1 = JsonWebKey.Factory.newJwk(kp1.getPublic()); + jwk1.setKeyId("key1"); + var jwk2 = JsonWebKey.Factory.newJwk(kp2.getPublic()); + jwk2.setKeyId("key2"); + + var keySet = new JsonWebKeySet(List.of(jwk1, jwk2)); when(jwtSecurity.actualJwtProducer()).thenReturn(JwtSecurity.JwtProducer.APIML); when(jwtSecurity.getPublicKeyInSet()).thenReturn(keySet); @@ -219,7 +269,6 @@ void givenMultipleKeys_thenReturnErrorWithCorrectMessage() throws Exception { lenient().when(mockApiMessage.mapToApiMessage()).thenReturn(expectedApiMessage); - Mono> result = controller.getPublicKeyUsedForSigning(); StepVerifier.create(result) @@ -232,24 +281,27 @@ void givenMultipleKeys_thenReturnErrorWithCorrectMessage() throws Exception { } @Test - void getPublicKeyUsedForSigning_joseException() throws Exception { - KeyPair keyPair = KeyPairGenerator.getInstance("RSA").generateKeyPair(); - RSAKey realRsaKey = new RSAKey.Builder((RSAPublicKey) keyPair.getPublic()).build(); + void whenNewJwkThrowsException_thenReturnsInternalServerError() throws Exception { + byte[] badModulus = new byte[]{0}; - RSAKey spyRsaKey = spy(realRsaKey); - doThrow(new JOSEException("Test JOSE Exception")).when(spyRsaKey).toPublicKey(); + var badKey = mock(RSAPublicKey.class); + when(badKey.getModulus()).thenReturn(new BigInteger(badModulus)); + when(badKey.getPublicExponent()).thenReturn(BigInteger.ONE); + lenient().when(badKey.getAlgorithm()).thenReturn("RSA"); + lenient().when(badKey.getFormat()).thenReturn(null); + lenient().when(badKey.getEncoded()).thenReturn(new byte[0]); - JWKSet keySet = new JWKSet(List.of(spyRsaKey)); + var badJwk = JsonWebKey.Factory.newJwk(badKey); when(jwtSecurity.actualJwtProducer()).thenReturn(JwtSecurity.JwtProducer.APIML); - when(jwtSecurity.getPublicKeyInSet()).thenReturn(keySet); + when(jwtSecurity.getPublicKeyInSet()).thenReturn(new JsonWebKeySet(List.of(badJwk))); ApiMessage expectedApiMessage = new ApiMessage("org.zowe.apiml.zaas.keys.unknown", MessageType.ERROR, "ZWEAG717E", "cnt", null, null); var mockApiMessage = mock(Message.class); when(messageService.createMessage("org.zowe.apiml.zaas.keys.unknown")).thenReturn(mockApiMessage); when(mockApiMessage.mapToApiMessage()).thenReturn(expectedApiMessage); - Mono> result = controller.getPublicKeyUsedForSigning(); + var result = controller.getPublicKeyUsedForSigning(); StepVerifier.create(result) .expectNextMatches(responseEntity -> { diff --git a/caching-service-package/src/main/resources/bin/start.sh b/caching-service-package/src/main/resources/bin/start.sh index 487ced9b46..7e275f8bd8 100755 --- a/caching-service-package/src/main/resources/bin/start.sh +++ b/caching-service-package/src/main/resources/bin/start.sh @@ -63,6 +63,12 @@ then ZWE_configs_spring_profiles_active="${ZWE_configs_spring_profiles_active}debug" fi +# script assumes it's in the caching-service component directory and jvm.security.override.properties needs to be relative path +JVM_SECURITY_PROPERTIES="" +if [ "${JVM_SECURITY_PROPERTIES_OVERRIDE:-false}" = "true" ]; then + JVM_SECURITY_PROPERTIES="-Djava.security.properties=../apiml-common-lib/bin/jvm.security.override.properties" +fi + if [ -z "${LIBRARY_PATH}" ] then LIBRARY_PATH="../common-java-lib/bin/" @@ -251,6 +257,7 @@ _BPX_JOBNAME=${ZWE_zowe_job_prefix}${CACHING_CODE} ${JAVA_BIN_DIR}java \ ${QUICK_START} \ ${ADD_OPENS} \ ${LOGBACK} \ + ${JVM_SECURITY_PROPERTIES_OVERRIDE} \ -Dibm.serversocket.recover=true \ -Dfile.encoding=UTF-8 \ -Dlogging.charset.console=${ZOWE_CONSOLE_LOG_CHARSET} \ diff --git a/discovery-package/src/main/resources/bin/start.sh b/discovery-package/src/main/resources/bin/start.sh index ac1b907856..49dfe9244d 100755 --- a/discovery-package/src/main/resources/bin/start.sh +++ b/discovery-package/src/main/resources/bin/start.sh @@ -65,6 +65,12 @@ else COMMON_LIB="${CMMN_LB}" fi +# script assumes it's in the discovery component directory and jvm.security.override.properties needs to be relative path +JVM_SECURITY_PROPERTIES="" +if [ "${JVM_SECURITY_PROPERTIES_OVERRIDE:-false}" = "true" ]; then + JVM_SECURITY_PROPERTIES="-Djava.security.properties=../apiml-common-lib/bin/jvm.security.override.properties" +fi + if [ -z "${LIBRARY_PATH}" ]; then LIBRARY_PATH="../common-java-lib/bin/" fi @@ -264,6 +270,7 @@ _BPX_JOBNAME=${ZWE_zowe_job_prefix}${DISCOVERY_CODE} ${JAVA_BIN_DIR}java \ ${QUICK_START} \ ${ADD_OPENS} \ ${LOGBACK} \ + ${JVM_SECURITY_PROPERTIES_OVERRIDE} \ -Dapiml.discovery.allPeersUrls=${ZWE_DISCOVERY_SERVICES_LIST} \ -Dapiml.discovery.password=password \ -Dapiml.discovery.serviceIdPrefixReplacer=${ZWE_configs_apiml_discovery_serviceIdPrefixReplacer} \ diff --git a/gateway-package/src/main/resources/bin/start.sh b/gateway-package/src/main/resources/bin/start.sh index ddf05542e6..d65a340581 100755 --- a/gateway-package/src/main/resources/bin/start.sh +++ b/gateway-package/src/main/resources/bin/start.sh @@ -89,13 +89,18 @@ else fi echo "jar file: "${JAR_FILE} # script assumes it's in the gateway component directory and common_lib needs to be relative path - if [ -z "${CMMN_LB}" ]; then COMMON_LIB="../apiml-common-lib/bin/api-layer-lite-lib-all.jar" else COMMON_LIB="${CMMN_LB}" fi +# script assumes it's in the gateway component directory and jvm.security.override.properties needs to be relative path +JVM_SECURITY_PROPERTIES="" +if [ "${JVM_SECURITY_PROPERTIES_OVERRIDE:-false}" = "true" ]; then + JVM_SECURITY_PROPERTIES="-Djava.security.properties=../apiml-common-lib/bin/jvm.security.override.properties" +fi + if [ -z "${LIBRARY_PATH}" ]; then LIBRARY_PATH="../common-java-lib/bin/" fi @@ -298,6 +303,7 @@ _BPX_JOBNAME=${ZWE_zowe_job_prefix}${GATEWAY_CODE} ${JAVA_BIN_DIR}java \ ${QUICK_START} \ ${ADD_OPENS} \ ${LOGBACK} \ + ${JVM_SECURITY_PROPERTIES_OVERRIDE} \ -Dapiml.connection.idleConnectionTimeoutSeconds=${ZWE_configs_apiml_connection_idleConnectionTimeoutSeconds:-5} \ -Dapiml.connection.timeout=${ZWE_configs_apiml_connection_timeout:-60000} \ -Dapiml.connection.timeToLive=${ZWE_configs_apiml_connection_timeToLive:-10000} \ diff --git a/gateway-service/build.gradle b/gateway-service/build.gradle index d56b1fa759..a8251e1e44 100644 --- a/gateway-service/build.gradle +++ b/gateway-service/build.gradle @@ -80,10 +80,9 @@ dependencies { } implementation libs.netty.reactor.http implementation libs.google.gson - implementation libs.jjwt - implementation libs.jjwt.impl - implementation libs.jjwt.jackson - implementation libs.nimbus.jose.jwt + + implementation libs.jose4j.jwt + implementation libs.bcpkix implementation libs.caffeine implementation libs.bucket4j.core diff --git a/gateway-service/src/main/java/org/zowe/apiml/gateway/loadbalancer/CustomLoadBalancerConfiguration.java b/gateway-service/src/main/java/org/zowe/apiml/gateway/loadbalancer/CustomLoadBalancerConfiguration.java index 11f8bd1f16..493a9bd3a9 100644 --- a/gateway-service/src/main/java/org/zowe/apiml/gateway/loadbalancer/CustomLoadBalancerConfiguration.java +++ b/gateway-service/src/main/java/org/zowe/apiml/gateway/loadbalancer/CustomLoadBalancerConfiguration.java @@ -10,13 +10,14 @@ package org.zowe.apiml.gateway.loadbalancer; -import io.jsonwebtoken.impl.DefaultClock; import org.springframework.beans.factory.annotation.Value; import org.springframework.cloud.loadbalancer.core.ServiceInstanceListSupplier; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.annotation.Bean; import org.zowe.apiml.gateway.caching.LoadBalancerCache; +import java.time.Clock; + /** * Configuration class for setting up the DeterministicRoutingListSupplierBuilder and StickySessionRoutingListSupplierBuilder * based on Gateway configuration. @@ -35,7 +36,7 @@ public ServiceInstanceListSupplier stickySessionServiceInstanceListSupplier( @Value("${instance.metadata.apiml.lb.cacheRecordExpirationTimeInHours:8}") int expirationTime) { return new DeterministicRoutingListSupplierBuilder(ServiceInstanceListSupplier.builder() .withDiscoveryClient()) - .withStickySessionRouting(cache, expirationTime, new DefaultClock()) + .withStickySessionRouting(cache, expirationTime, Clock.systemUTC()) .build(context); } diff --git a/gateway-service/src/main/java/org/zowe/apiml/gateway/loadbalancer/DeterministicLoadBalancer.java b/gateway-service/src/main/java/org/zowe/apiml/gateway/loadbalancer/DeterministicLoadBalancer.java index 038e19012a..ee5e756a83 100644 --- a/gateway-service/src/main/java/org/zowe/apiml/gateway/loadbalancer/DeterministicLoadBalancer.java +++ b/gateway-service/src/main/java/org/zowe/apiml/gateway/loadbalancer/DeterministicLoadBalancer.java @@ -10,10 +10,10 @@ package org.zowe.apiml.gateway.loadbalancer; -import io.jsonwebtoken.Claims; -import io.jsonwebtoken.Clock; -import io.jsonwebtoken.Jwts; -import io.jsonwebtoken.MalformedJwtException; +import com.nimbusds.jwt.JWTClaimsSet; +import com.nimbusds.jwt.JWTParser; +import com.nimbusds.jwt.proc.BadJWTException; +import com.nimbusds.jwt.proc.ExpiredJWTException; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.springframework.cloud.client.ServiceInstance; @@ -32,6 +32,8 @@ import reactor.core.publisher.Mono; import java.nio.charset.StandardCharsets; +import java.text.ParseException; +import java.time.Clock; import java.time.LocalDateTime; import java.util.Base64; import java.util.Collections; @@ -55,18 +57,18 @@ public class DeterministicLoadBalancer extends SameInstancePreferenceServiceInst private static final String HEADER_NONE_SIGNATURE = Base64.getEncoder().encodeToString("{\"typ\":\"JWT\",\"alg\":\"none\"}".getBytes(StandardCharsets.UTF_8)); private final LoadBalancerCache cache; - private final Clock clock; private final int expirationTime; + private final Clock clock; public DeterministicLoadBalancer(ServiceInstanceListSupplier delegate, ReactiveLoadBalancer.Factory loadBalancerClientFactory, LoadBalancerCache cache, - Clock clock, - int expirationTime) { + int expirationTime, + Clock clock) { super(delegate, loadBalancerClientFactory); this.cache = cache; - this.clock = clock; this.expirationTime = expirationTime; + this.clock = clock; log.debug("StickySessionLoadBalancer instantiated"); } @@ -264,38 +266,41 @@ private boolean lbTypeIsAuthentication(ServiceInstance instance) { return false; } - private String removeJwtSign(String jwtToken) { + private String removeJwtSign(String jwtToken) throws BadJWTException { if (jwtToken == null) return null; int firstDot = jwtToken.indexOf('.'); int lastDot = jwtToken.lastIndexOf('.'); - if ((firstDot < 0) || (firstDot >= lastDot)) throw new MalformedJwtException("Invalid JWT format"); + if ((firstDot < 0) || (firstDot >= lastDot)) { + throw new BadJWTException("Invalid JWT format"); + } return HEADER_NONE_SIGNATURE + jwtToken.substring(firstDot, lastDot + 1); } - private Claims getJwtClaims(String jwt) { + private JWTClaimsSet getJwtClaims(String jwt) { /* * Removes signature, because we don't have key to verify z/OS tokens, and we just need to read claim. * Verification is done by SAF itself. JWT library doesn't parse signed key without verification. */ try { - String withoutSign = removeJwtSign(jwt); - return Jwts.parser() - .unsecured() - .clock(clock) - .build() - .parseUnsecuredClaims(withoutSign) - .getPayload(); - } catch (RuntimeException exception) { - log.debug("Exception when trying to parse the JWT token {}", jwt); + var jwtWithoutSignature = removeJwtSign(jwt); + + var claims = JWTParser.parse(jwtWithoutSignature) + .getJWTClaimsSet(); + if (claims.getExpirationTime().toInstant().isBefore(clock.instant())) { + throw new ExpiredJWTException("JWT Token is expired"); + } + return claims; + } catch (RuntimeException | ParseException | BadJWTException exception) { + log.debug("Exception when trying to parse the JWT token {}: {}", jwt, exception.getMessage()); return null; // NOSONAR } } private String extractSubFromToken(String token) { if (StringUtils.isNotEmpty(token)) { - Claims claims = getJwtClaims(token); + var claims = getJwtClaims(token); if (claims != null) { return claims.getSubject(); } diff --git a/gateway-service/src/main/java/org/zowe/apiml/gateway/loadbalancer/DeterministicRoutingListSupplierBuilder.java b/gateway-service/src/main/java/org/zowe/apiml/gateway/loadbalancer/DeterministicRoutingListSupplierBuilder.java index c2cf094fda..c99b1eaa52 100644 --- a/gateway-service/src/main/java/org/zowe/apiml/gateway/loadbalancer/DeterministicRoutingListSupplierBuilder.java +++ b/gateway-service/src/main/java/org/zowe/apiml/gateway/loadbalancer/DeterministicRoutingListSupplierBuilder.java @@ -10,12 +10,13 @@ package org.zowe.apiml.gateway.loadbalancer; -import io.jsonwebtoken.Clock; import lombok.RequiredArgsConstructor; import org.springframework.cloud.loadbalancer.core.ServiceInstanceListSupplierBuilder; import org.springframework.cloud.loadbalancer.support.LoadBalancerClientFactory; import org.zowe.apiml.gateway.caching.LoadBalancerCache; +import java.time.Clock; + @RequiredArgsConstructor public class DeterministicRoutingListSupplierBuilder { @@ -24,7 +25,7 @@ public class DeterministicRoutingListSupplierBuilder { public ServiceInstanceListSupplierBuilder withStickySessionRouting(LoadBalancerCache cache, int expirationTime, Clock clock) { ServiceInstanceListSupplierBuilder.DelegateCreator creator = (context, delegate) -> { LoadBalancerClientFactory loadBalancerClientFactory = context.getBean(LoadBalancerClientFactory.class); - return new DeterministicLoadBalancer(delegate, loadBalancerClientFactory, cache, clock, expirationTime); + return new DeterministicLoadBalancer(delegate, loadBalancerClientFactory, cache, expirationTime, clock); }; builder.with(creator); return builder; diff --git a/gateway-service/src/test/java/org/zowe/apiml/gateway/loadbalancer/DeterministicLoadBalancerTest.java b/gateway-service/src/test/java/org/zowe/apiml/gateway/loadbalancer/DeterministicLoadBalancerTest.java index eedd65335f..7eed522b64 100644 --- a/gateway-service/src/test/java/org/zowe/apiml/gateway/loadbalancer/DeterministicLoadBalancerTest.java +++ b/gateway-service/src/test/java/org/zowe/apiml/gateway/loadbalancer/DeterministicLoadBalancerTest.java @@ -10,7 +10,6 @@ package org.zowe.apiml.gateway.loadbalancer; -import io.jsonwebtoken.Clock; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; @@ -36,10 +35,10 @@ import reactor.core.publisher.Mono; import reactor.test.StepVerifier; +import java.time.Clock; import java.time.Instant; import java.time.LocalDateTime; import java.util.ArrayList; -import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -101,7 +100,7 @@ void setUp() { when(factory.getProperties(any())).thenReturn(properties); when(delegate.getServiceId()).thenReturn("service"); when(delegate.get(request)).thenReturn(Flux.just(defaultServiceInstancesList)); - this.loadBalancer = new DeterministicLoadBalancer(delegate, factory, lbCache, clock, DEFAULT_EXPIRATION_HS); + this.loadBalancer = new DeterministicLoadBalancer(delegate, factory, lbCache, DEFAULT_EXPIRATION_HS, clock); } @Nested @@ -157,7 +156,7 @@ void setUp() { when(requestData.getCookies()).thenReturn(cookie); when(request.getContext()).thenReturn(context); - when(clock.now()).thenReturn(Date.from(Instant.ofEpochSecond(1721552753))); + when(clock.instant()).thenReturn(Instant.ofEpochSecond(1721552753)); } @Test @@ -317,7 +316,7 @@ void setUp() { when(requestData.getHeaders()).thenReturn(headers); when(requestData.getCookies()).thenReturn(cookie); when(request.getContext()).thenReturn(context); - when(clock.now()).thenReturn(Date.from(Instant.ofEpochSecond(1721552753))); + when(clock.instant()).thenReturn(Instant.ofEpochSecond(1721552753)); } @Nested diff --git a/gradle/versions.gradle b/gradle/versions.gradle index efaa8bdce6..0abcb34500 100644 --- a/gradle/versions.gradle +++ b/gradle/versions.gradle @@ -64,6 +64,7 @@ dependencyResolutionManagement { version('jettison', '1.5.4') //0.12.x version contains breaking changes version('jjwt', '0.13.0') + version('jose4j', '0.9.6') version('jodaTime', '2.14.0') version('jsonPath', '2.9.0') version('jsonSmart', '2.6.0') @@ -205,6 +206,7 @@ dependencyResolutionManagement { library('jjwt', 'io.jsonwebtoken', 'jjwt-api').versionRef('jjwt') library('jjwt_impl', 'io.jsonwebtoken', 'jjwt-impl').versionRef('jjwt') library('jjwt_jackson', 'io.jsonwebtoken', 'jjwt-jackson').versionRef('jjwt') + library('jose4j_jwt', 'org.bitbucket.b_c', 'jose4j').versionRef('jose4j') library('json_path', 'com.jayway.jsonpath', 'json-path').versionRef('jsonPath') library('junit_jupiter', 'org.junit.jupiter', 'junit-jupiter').versionRef('junitJupiter') library('junit_platform_launcher', 'org.junit.platform', 'junit-platform-launcher').versionRef('junitPlatform') diff --git a/zaas-package/src/main/resources/bin/start.sh b/zaas-package/src/main/resources/bin/start.sh index 3892b0c99c..e6ebcc4906 100755 --- a/zaas-package/src/main/resources/bin/start.sh +++ b/zaas-package/src/main/resources/bin/start.sh @@ -81,8 +81,8 @@ else JAR_FILE="$(pwd)/bin/zaas-service-lite.jar" fi echo "jar file: "${JAR_FILE} -# script assumes it's in the ZAAS component directory and common_lib needs to be relative path +# script assumes it's in the ZAAS component directory and common_lib needs to be relative path if [ -z "${CMMN_LB}" ] then COMMON_LIB="../apiml-common-lib/bin/api-layer-lite-lib-all.jar" @@ -90,6 +90,12 @@ else COMMON_LIB=${CMMN_LB} fi +# script assumes it's in the zaas component directory and jvm.security.override.properties needs to be relative path +JVM_SECURITY_PROPERTIES="" +if [ "${JVM_SECURITY_PROPERTIES_OVERRIDE:-false}" = "true" ]; then + JVM_SECURITY_PROPERTIES="-Djava.security.properties=../apiml-common-lib/bin/jvm.security.override.properties" +fi + if [ -z "${LIBRARY_PATH}" ] then LIBRARY_PATH="../common-java-lib/bin/" @@ -317,6 +323,7 @@ _BPX_JOBNAME=${ZWE_zowe_job_prefix}${ZAAS_CODE} ${JAVA_BIN_DIR}java \ ${QUICK_START} \ ${ADD_OPENS} \ ${LOGBACK} \ + ${JVM_SECURITY_PROPERTIES_OVERRIDE} \ -Dibm.serversocket.recover=true \ -Dfile.encoding=UTF-8 \ -Dlogging.charset.console=${ZOWE_CONSOLE_LOG_CHARSET} \ diff --git a/zaas-service/build.gradle b/zaas-service/build.gradle index de809c8f40..9ea505c415 100644 --- a/zaas-service/build.gradle +++ b/zaas-service/build.gradle @@ -74,8 +74,6 @@ dependencies { implementation libs.jackson.databind implementation libs.jaxbApi implementation libs.spring.cloud.commons - implementation libs.jjwt - implementation libs.nimbus.jose.jwt implementation libs.spring.doc implementation libs.swagger3.parser @@ -99,8 +97,8 @@ dependencies { implementation libs.caffeine - implementation libs.jjwt.impl - implementation libs.jjwt.jackson + implementation libs.nimbus.jose.jwt // Parsing + implementation libs.jose4j.jwt // Signing, with support for JCA with ICSF compileOnly libs.lombok annotationProcessor libs.lombok @@ -114,11 +112,15 @@ dependencies { testImplementation libs.rest.assured testImplementation libs.rest.assured.json.path testImplementation libs.rest.assured.xml.path + testImplementation libs.jjwt + testImplementation libs.jjwt.impl + testImplementation libs.jjwt.jackson testCompileOnly libs.lombok testAnnotationProcessor libs.lombok testImplementation(testFixtures(project(":apiml-common"))) + testImplementation(testFixtures(project(":apiml-security-common"))) } bootJar { diff --git a/zaas-service/src/main/java/org/zowe/apiml/zaas/controllers/AuthController.java b/zaas-service/src/main/java/org/zowe/apiml/zaas/controllers/AuthController.java index 6a498631af..dd4df09a00 100644 --- a/zaas-service/src/main/java/org/zowe/apiml/zaas/controllers/AuthController.java +++ b/zaas-service/src/main/java/org/zowe/apiml/zaas/controllers/AuthController.java @@ -13,9 +13,6 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectWriter; -import com.nimbusds.jose.JOSEException; -import com.nimbusds.jose.jwk.JWK; -import com.nimbusds.jose.jwk.JWKSet; import io.swagger.v3.oas.annotations.Hidden; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.media.Content; @@ -31,13 +28,24 @@ import lombok.extern.slf4j.Slf4j; import org.bouncycastle.util.io.pem.PemObject; import org.bouncycastle.util.io.pem.PemWriter; +import org.jose4j.jwk.JsonWebKey; +import org.jose4j.jwk.JsonWebKeySet; +import org.jose4j.jwk.RsaJsonWebKey; +import org.jose4j.lang.JoseException; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; 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.message.api.ApiMessageView; import org.zowe.apiml.message.core.MessageService; import org.zowe.apiml.security.common.token.AccessTokenProvider; @@ -53,9 +61,14 @@ 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 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 contain method for user and also method for calling services @@ -341,26 +354,27 @@ public void distributeInvalidate(HttpServletRequest request, HttpServletResponse @ApiResponse(responseCode = "200", description = "OK", content = @Content( mediaType = MediaType.APPLICATION_JSON_VALUE, - schema = @Schema(implementation = JWKSet.class) + schema = @Schema(implementation = JsonWebKeySet.class) ) ) }) - public Map getAllPublicKeys() { - List keys; + public ResponseEntity getAllPublicKeys() { + List keys; + if (jwtSecurity.actualJwtProducer() == JwtSecurity.JwtProducer.ZOSMF) { - keys = new LinkedList<>(zosmfService.getPublicKeys().getKeys()); + keys = new LinkedList<>(zosmfService.getPublicKeys().getJsonWebKeys()); } else { keys = new LinkedList<>(); } - Optional key = jwtSecurity.getJwkPublicKey(); + var key = jwtSecurity.getJwkPublicKey(); key.ifPresent(keys::add); if ((oidcProvider != null) && (oidcProvider instanceof OIDCTokenProvider oidcTokenProvider)) { - JWKSet oidcSet = oidcTokenProvider.getJwkSet(); + var oidcSet = oidcTokenProvider.getJwkSet(); if (oidcSet != null) { - keys.addAll(oidcSet.getKeys()); + keys.addAll(oidcSet.getJsonWebKeys()); } } - return new JWKSet(keys).toJSONObject(true); + return ResponseEntity.ok().body(new JsonWebKeySet(keys).toJson()); } /** @@ -380,13 +394,13 @@ public Map getAllPublicKeys() { @ApiResponse(responseCode = "200", description = "OK", content = @Content( mediaType = MediaType.APPLICATION_JSON_VALUE, - schema = @Schema(implementation = JWKSet.class) + schema = @Schema(implementation = JsonWebKeySet.class) ) ) }) - public Map getCurrentPublicKeys() { - final List keys = getCurrentKey(); - return new JWKSet(keys).toJSONObject(true); + public ResponseEntity getCurrentPublicKeys() { + final List keys = getCurrentKey(); + return ResponseEntity.ok(new JsonWebKeySet(keys).toJson()); } /** @@ -413,7 +427,7 @@ public Map getCurrentPublicKeys() { ) }) public ResponseEntity getPublicKeyUsedForSigning() { - List publicKeys = getCurrentKey(); + var publicKeys = getCurrentKey(); if (publicKeys.isEmpty()) { log.debug("JWT setup was not yet initialized so there is no public key for response."); return new ResponseEntity<>(messageService.createMessage("org.zowe.apiml.zaas.keys.unknownState").mapToApiMessage(), HttpStatus.INTERNAL_SERVER_ERROR); @@ -423,20 +437,19 @@ public ResponseEntity getPublicKeyUsedForSigning() { return new ResponseEntity<>(messageService.createMessage("org.zowe.apiml.zaas.keys.wrongAmount", publicKeys.size()).mapToApiMessage(), HttpStatus.INTERNAL_SERVER_ERROR); } try { - PublicKey key = publicKeys.get(0) - .toRSAKey() - .toPublicKey(); + RsaJsonWebKey jwk = (RsaJsonWebKey) JsonWebKey.Factory.newJwk(publicKeys.get(0).toJson()); + PublicKey key = jwk.getPublicKey(); return new ResponseEntity<>(getPublicKeyAsPem(key), HttpStatus.OK); - } catch (IOException | JOSEException ex) { + } catch (IOException | JoseException ex) { log.error("It was not possible to get public key for JWK, exception message: {}", ex.getMessage()); return new ResponseEntity<>(messageService.createMessage("org.zowe.apiml.zaas.keys.unknown").mapToApiMessage(), HttpStatus.INTERNAL_SERVER_ERROR); } } - private List getCurrentKey() { + private List getCurrentKey() { JwtSecurity.JwtProducer producer = jwtSecurity.actualJwtProducer(); - JWKSet currentKey; + JsonWebKeySet currentKey; switch (producer) { case ZOSMF: currentKey = zosmfService.getPublicKeys(); @@ -448,7 +461,7 @@ private List getCurrentKey() { //return 500 as we just don't know yet. return Collections.emptyList(); } - return currentKey.getKeys(); + return currentKey.getJsonWebKeys(); } @PostMapping(path = OIDC_TOKEN_VALIDATE) diff --git a/zaas-service/src/main/java/org/zowe/apiml/zaas/security/service/AuthenticationService.java b/zaas-service/src/main/java/org/zowe/apiml/zaas/security/service/AuthenticationService.java index 3e180e3a1c..1531d8950c 100644 --- a/zaas-service/src/main/java/org/zowe/apiml/zaas/security/service/AuthenticationService.java +++ b/zaas-service/src/main/java/org/zowe/apiml/zaas/security/service/AuthenticationService.java @@ -13,11 +13,13 @@ import com.netflix.appinfo.InstanceInfo; import com.netflix.discovery.EurekaClient; import com.netflix.discovery.shared.Application; -import io.jsonwebtoken.Claims; -import io.jsonwebtoken.JwsHeader; -import io.jsonwebtoken.Jwt; -import io.jsonwebtoken.Jwts; -import io.jsonwebtoken.security.SignatureException; +import com.nimbusds.jose.JOSEException; +import com.nimbusds.jose.crypto.RSASSAVerifier; +import com.nimbusds.jwt.JWTClaimsSet; +import com.nimbusds.jwt.JWTParser; +import com.nimbusds.jwt.SignedJWT; +import com.nimbusds.jwt.proc.BadJWTException; +import com.nimbusds.jwt.proc.ExpiredJWTException; import jakarta.annotation.PostConstruct; import jakarta.servlet.http.Cookie; import jakarta.servlet.http.HttpServletRequest; @@ -25,6 +27,12 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang.StringUtils; +import org.jose4j.jws.AlgorithmIdentifiers; +import org.jose4j.jws.JsonWebSignature; +import org.jose4j.jwt.JwtClaims; +import org.jose4j.jwt.NumericDate; +import org.jose4j.lang.JoseException; +import org.jose4j.lang.UncheckedJoseException; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.cache.CacheManager; import org.springframework.cache.annotation.CacheEvict; @@ -42,17 +50,31 @@ import org.zowe.apiml.constants.ApimlConstants; import org.zowe.apiml.product.constants.CoreService; import org.zowe.apiml.security.common.config.AuthConfigurationProperties; -import org.zowe.apiml.security.common.token.*; +import org.zowe.apiml.security.common.token.QueryResponse; +import org.zowe.apiml.security.common.token.TokenAuthentication; +import org.zowe.apiml.security.common.token.TokenExpireException; +import org.zowe.apiml.security.common.token.TokenFormatNotValidException; +import org.zowe.apiml.security.common.token.TokenNotValidException; import org.zowe.apiml.util.CacheUtils; import org.zowe.apiml.util.EurekaUtils; import org.zowe.apiml.zaas.controllers.AuthController; import org.zowe.apiml.zaas.security.service.schema.source.AuthSource; import org.zowe.apiml.zaas.security.service.zosmf.ZosmfService; -import java.util.*; - -import static org.zowe.apiml.zaas.security.service.JwtUtils.getJwtClaims; -import static org.zowe.apiml.zaas.security.service.JwtUtils.handleJwtParserException; +import java.security.interfaces.RSAPublicKey; +import java.text.ParseException; +import java.time.Clock; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; + +import static org.zowe.apiml.security.common.util.JwtUtils.getJwtClaims; +import static org.zowe.apiml.security.common.util.JwtUtils.handleJwtParserException; import static org.zowe.apiml.zaas.security.service.zosmf.ZosmfService.TokenType.JWT; import static org.zowe.apiml.zaas.security.service.zosmf.ZosmfService.TokenType.LTPA; @@ -82,6 +104,7 @@ public class AuthenticationService { private final RestTemplate restTemplate; private final CacheManager cacheManager; private final CacheUtils cacheUtils; + private final Clock clock; // to force calling inside methods with aspects - ie. ehCache aspect private AuthenticationService meAsProxy; @@ -121,26 +144,36 @@ public String createLongLivedJwtToken(@NonNull String username, int daysToLive, } private String createJWT(String username, String issuer, Map claims, long issuedAt, long expiration) { - return Jwts.builder() - .subject(username) - .issuedAt(new Date(issuedAt)) - .expiration(new Date(expiration)) - .issuer(issuer) - .id(UUID.randomUUID().toString()) - .claims(claims) - .signWith(jwtSecurityInitializer.getJwtSecret(), jwtSecurityInitializer.getSignatureAlgorithm()).compact(); + try { + var newClaims = new JwtClaims(); + + newClaims.setIssuer(issuer); // who creates the token and signs it + newClaims.setExpirationTime(NumericDate.fromMilliseconds(expiration)); + newClaims.setGeneratedJwtId(); + newClaims.setIssuedAt(NumericDate.fromMilliseconds(issuedAt)); + newClaims.setSubject(username); + if (claims != null) { + claims.entrySet().forEach(entry -> newClaims.setClaim(entry.getKey(), entry.getValue())); + } + + var jws = new JsonWebSignature(); + jws.setPayload(newClaims.toJson()); + jws.setKey(jwtSecurityInitializer.getJwtSecret()); + jws.setHeader("typ", "JWT"); + jws.setAlgorithmHeaderValue(AlgorithmIdentifiers.RSA_USING_SHA256); + jws.setDoKeyValidation(false); + return jws.getCompactSerialization(); + } catch (JoseException e) { + throw new UncheckedJoseException(e.getMessage(), e); + } } @SuppressWarnings("java:S5659") // It is checking the signature securely - https://github.com/zowe/api-layer/issues/3191 - public QueryResponse parseJwtWithSignature(String jwt) throws SignatureException { + public QueryResponse parseJwtWithSignature(String jwt) { try { - Jwt parsedJwt = (Jwt) Jwts.parser() - .verifyWith(jwtSecurityInitializer.getJwtPublicKey()) - .build() - .parse(jwt); - - return parseQueryResponse(parsedJwt.getPayload()); + var claims = validateAndParseLocalJwtToken(jwt); + return parseQueryResponse(claims); } catch (RuntimeException exception) { throw handleJwtParserException(exception); } @@ -263,15 +296,27 @@ public Boolean isInvalidated(String jwtToken) { return Boolean.FALSE; } - - private Claims validateAndParseLocalJwtToken(String jwtToken) { + private JWTClaimsSet validateAndParseLocalJwtToken(String jwtToken) { try { - return Jwts.parser() - .verifyWith(jwtSecurityInitializer.getJwtPublicKey()) - .build() - .parseSignedClaims(jwtToken) - .getPayload(); - } catch (RuntimeException exception) { + var parsedJwt = JWTParser.parse(jwtToken); + if (parsedJwt instanceof SignedJWT signedJwt) { + var rsaVerifier = new RSASSAVerifier((RSAPublicKey) jwtSecurityInitializer.getJwtPublicKey()); + var verified = signedJwt.verify(rsaVerifier); + if (verified) { + var claims = parsedJwt.getJWTClaimsSet(); + if (claims.getExpirationTime().toInstant().isBefore(clock.instant())) { + log.debug("OIDC Token is expired"); + throw new ExpiredJWTException("OIDC Token is expired"); + } + return claims; + } + throw new BadJWTException("Token signature is invalid for public key"); + } else { + throw new BadJWTException("Token is not signed"); + } + } catch (ParseException e) { + throw handleJwtParserException(new BadJWTException(e.getMessage())); + } catch (RuntimeException | BadJWTException | JOSEException exception) { throw handleJwtParserException(exception); } } @@ -379,25 +424,29 @@ public TokenAuthentication validateJwtToken(TokenAuthentication token) { * @return the query response */ public QueryResponse parseJwtToken(String jwtToken) { - Claims claims = getJwtClaims(jwtToken); + var claims = getJwtClaims(jwtToken); return parseQueryResponse(claims); } - public QueryResponse parseQueryResponse(Claims claims) { - Object scopesObject = claims.get(SCOPES); + public QueryResponse parseQueryResponse(JWTClaimsSet claims) { + Object scopesObject = claims.getClaim(SCOPES); List scopes = Collections.emptyList(); if (scopesObject instanceof List) { scopes = (List) scopesObject; } - return new QueryResponse( - claims.get(DOMAIN_CLAIM_NAME, String.class), - claims.getSubject(), - claims.getIssuedAt(), - claims.getExpiration(), - claims.getIssuer(), - scopes, - QueryResponse.Source.valueByIssuer(claims.getIssuer()) - ); + try { + return new QueryResponse( + claims.getClaimAsString(DOMAIN_CLAIM_NAME), + claims.getSubject(), + claims.getIssueTime(), + claims.getExpirationTime(), + claims.getIssuer(), + scopes, + QueryResponse.Source.valueByIssuer(claims.getIssuer()) + ); + } catch (ParseException e) { + throw new TokenNotValidException(e.getMessage(), e); + } } /** @@ -407,7 +456,7 @@ public QueryResponse parseQueryResponse(Claims claims) { * @return AuthSource.Origin value based on the iss token claim. */ public AuthSource.Origin getTokenOrigin(String jwtToken) { - Claims claims = getJwtClaims(jwtToken); + var claims = getJwtClaims(jwtToken); QueryResponse.Source source = QueryResponse.Source.valueByIssuer(claims.getIssuer()); return AuthSource.Origin.valueByTokenSource(source); } @@ -420,7 +469,11 @@ public AuthSource.Origin getTokenOrigin(String jwtToken) { * @return LTPA token extracted from JWT */ public String getLtpaTokenWithValidation(String jwtToken) { - return validateAndParseLocalJwtToken(jwtToken).get(LTPA_CLAIM_NAME, String.class); + try { + return validateAndParseLocalJwtToken(jwtToken).getClaimAsString(LTPA_CLAIM_NAME); + } catch (ParseException e) { + throw new TokenNotValidException(e.getMessage(), e); + } } /** @@ -431,9 +484,13 @@ public String getLtpaTokenWithValidation(String jwtToken) { * @throws TokenNotValidException if the JWT token is not valid */ public String getLtpaToken(String jwtToken) { - Claims claims = getJwtClaims(jwtToken); + var claims = getJwtClaims(jwtToken); - return claims.get(LTPA_CLAIM_NAME, String.class); + try { + return claims.getClaimAsString(LTPA_CLAIM_NAME); + } catch (ParseException e) { + throw new TokenNotValidException(e.getMessage(), e); + } } /** diff --git a/zaas-service/src/main/java/org/zowe/apiml/zaas/security/service/JwtSecurity.java b/zaas-service/src/main/java/org/zowe/apiml/zaas/security/service/JwtSecurity.java index cc4cf7f831..25f9cea8d8 100644 --- a/zaas-service/src/main/java/org/zowe/apiml/zaas/security/service/JwtSecurity.java +++ b/zaas-service/src/main/java/org/zowe/apiml/zaas/security/service/JwtSecurity.java @@ -15,14 +15,13 @@ import com.netflix.discovery.EurekaClient; import com.netflix.discovery.EurekaEvent; import com.netflix.discovery.EurekaEventListener; -import com.nimbusds.jose.jwk.JWK; -import com.nimbusds.jose.jwk.JWKSet; -import com.nimbusds.jose.jwk.RSAKey; -import io.jsonwebtoken.Jwts; -import io.jsonwebtoken.security.SignatureAlgorithm; +import com.nimbusds.jose.JWSAlgorithm; import jakarta.annotation.PostConstruct; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang.StringUtils; +import org.jose4j.jwk.JsonWebKey; +import org.jose4j.jwk.JsonWebKeySet; +import org.jose4j.lang.JoseException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; @@ -37,7 +36,14 @@ import java.security.PublicKey; import java.security.interfaces.RSAPublicKey; import java.time.Duration; -import java.util.*; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.Timer; +import java.util.TimerTask; /** * JWT Security related configuration. Distinguishes between methods used to generate JWT tokens provided by ZAAS. @@ -65,7 +71,7 @@ public class JwtSecurity { @Value("${apiml.security.jwtInitializerTimeout:5}") private int timeout; - private SignatureAlgorithm signatureAlgorithm; + private JWSAlgorithm signatureAlgorithm; private PrivateKey jwtSecret; private PublicKey jwtPublicKey; @@ -164,7 +170,7 @@ public JwtProducer actualJwtProducer(boolean isLtpaSupported) { * Load the JWT secret. If there is a configuration issue the keys are not loaded and the error is logged. */ private void loadJwtSecret() { - signatureAlgorithm = Jwts.SIG.RS256; + signatureAlgorithm = JWSAlgorithm.RS256; HttpsConfig config = currentConfig(); try { @@ -216,7 +222,8 @@ private void validateInitializationAgainstZosmf() { /* * Start of the actual API for the security class */ - public SignatureAlgorithm getSignatureAlgorithm() { + @VisibleForTesting + public JWSAlgorithm getSignatureAlgorithm() { return signatureAlgorithm; } @@ -228,22 +235,25 @@ public PublicKey getJwtPublicKey() { return jwtPublicKey; } - public JWKSet getPublicKeyInSet() { - final List keys = new LinkedList<>(); + public JsonWebKeySet getPublicKeyInSet() { + List keys = new ArrayList<>(); - Optional publicKey = getJwkPublicKey(); - publicKey.ifPresent(keys::add); + var publicKeyOptional = getJwkPublicKey(); + publicKeyOptional.ifPresent(keys::add); - return new JWKSet(keys); + return new JsonWebKeySet(keys); } - public Optional getJwkPublicKey() { + public Optional getJwkPublicKey() { if (jwtPublicKey instanceof RSAPublicKey rsaPublicKey) { - return Optional.of( - new RSAKey.Builder(rsaPublicKey).build().toPublicJWK() - ); + try { + return Optional.of(JsonWebKey.Factory.newJwk(rsaPublicKey)); + } catch (JoseException e) { + log.debug("Unable to create JWK {}", e.getMessage(), e); + } + } else { + log.debug("Unsupported type of public key: {}", jwtPublicKey == null ? null : jwtPublicKey.getClass()); } - log.debug("Unsupported type of public key: {}", jwtPublicKey == null ? null : jwtPublicKey.getClass()); return Optional.empty(); } diff --git a/zaas-service/src/main/java/org/zowe/apiml/zaas/security/service/ModulithAuthenticationService.java b/zaas-service/src/main/java/org/zowe/apiml/zaas/security/service/ModulithAuthenticationService.java index 9f82203bc9..770da121d1 100644 --- a/zaas-service/src/main/java/org/zowe/apiml/zaas/security/service/ModulithAuthenticationService.java +++ b/zaas-service/src/main/java/org/zowe/apiml/zaas/security/service/ModulithAuthenticationService.java @@ -26,6 +26,8 @@ import org.zowe.apiml.util.EurekaUtils; import org.zowe.apiml.zaas.security.service.zosmf.ZosmfService; +import java.time.Clock; + @Slf4j @Service @Scope(proxyMode = ScopedProxyMode.TARGET_CLASS) @@ -38,7 +40,7 @@ public ModulithAuthenticationService(ApplicationContext applicationContext, ZosmfService zosmfService, EurekaClient eurekaClient, RestTemplate restTemplate, CacheManager cacheManager, CacheUtils cacheUtils) { super(applicationContext, authConfigurationProperties, jwtSecurityInitializer, zosmfService, eurekaClient, restTemplate, - cacheManager, cacheUtils); + cacheManager, cacheUtils, Clock.systemUTC()); } @Override diff --git a/zaas-service/src/main/java/org/zowe/apiml/zaas/security/service/schema/source/OIDCAuthSourceService.java b/zaas-service/src/main/java/org/zowe/apiml/zaas/security/service/schema/source/OIDCAuthSourceService.java index 2fe0839d01..a10a2d0f15 100644 --- a/zaas-service/src/main/java/org/zowe/apiml/zaas/security/service/schema/source/OIDCAuthSourceService.java +++ b/zaas-service/src/main/java/org/zowe/apiml/zaas/security/service/schema/source/OIDCAuthSourceService.java @@ -35,7 +35,7 @@ import java.util.Optional; import java.util.function.Function; -import static org.zowe.apiml.zaas.security.service.JwtUtils.getFieldValuesFromToken; +import static org.zowe.apiml.security.common.util.JwtUtils.getFieldValuesFromToken; @Service @RequiredArgsConstructor diff --git a/zaas-service/src/main/java/org/zowe/apiml/zaas/security/service/token/HttpsJwksProvider.java b/zaas-service/src/main/java/org/zowe/apiml/zaas/security/service/token/HttpsJwksProvider.java new file mode 100644 index 0000000000..9b39572f67 --- /dev/null +++ b/zaas-service/src/main/java/org/zowe/apiml/zaas/security/service/token/HttpsJwksProvider.java @@ -0,0 +1,23 @@ +/* + * 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.zaas.security.service.token; + +import org.jose4j.jwk.HttpsJwks; +import org.springframework.stereotype.Component; + +@Component +public class HttpsJwksProvider { + + public HttpsJwks getFor(String url) { + return new HttpsJwks(url); + } + +} diff --git a/zaas-service/src/main/java/org/zowe/apiml/zaas/security/service/token/JWKResolver.java b/zaas-service/src/main/java/org/zowe/apiml/zaas/security/service/token/JWKResolver.java new file mode 100644 index 0000000000..383884f625 --- /dev/null +++ b/zaas-service/src/main/java/org/zowe/apiml/zaas/security/service/token/JWKResolver.java @@ -0,0 +1,31 @@ +/* + * 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.zaas.security.service.token; + +import lombok.RequiredArgsConstructor; +import org.jose4j.jwk.JsonWebKeySet; +import org.jose4j.lang.JoseException; +import org.springframework.stereotype.Component; + +import java.io.IOException; + +@Component +@RequiredArgsConstructor +public class JWKResolver { + + private final HttpsJwksProvider provider; + + public JsonWebKeySet resolve(String url) throws JoseException, IOException { + var httpsJwks = provider.getFor(url); + return new JsonWebKeySet(httpsJwks.getJsonWebKeys()); + } + +} diff --git a/zaas-service/src/main/java/org/zowe/apiml/zaas/security/service/token/OIDCConfig.java b/zaas-service/src/main/java/org/zowe/apiml/zaas/security/service/token/OIDCConfig.java index be19d7ace1..3c7f94db62 100644 --- a/zaas-service/src/main/java/org/zowe/apiml/zaas/security/service/token/OIDCConfig.java +++ b/zaas-service/src/main/java/org/zowe/apiml/zaas/security/service/token/OIDCConfig.java @@ -13,18 +13,18 @@ import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; -import io.jsonwebtoken.Clock; -import io.jsonwebtoken.impl.DefaultClock; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Primary; +import java.time.Clock; + @Configuration public class OIDCConfig { @Bean("oidcJwtClock") public Clock oidcJwtClock() { - return new DefaultClock(); + return Clock.systemUTC(); } @Bean("oidcJwkMapper") diff --git a/zaas-service/src/main/java/org/zowe/apiml/zaas/security/service/token/OIDCTokenProvider.java b/zaas-service/src/main/java/org/zowe/apiml/zaas/security/service/token/OIDCTokenProvider.java index 42bcf2694c..c8b1eccda8 100644 --- a/zaas-service/src/main/java/org/zowe/apiml/zaas/security/service/token/OIDCTokenProvider.java +++ b/zaas-service/src/main/java/org/zowe/apiml/zaas/security/service/token/OIDCTokenProvider.java @@ -10,15 +10,13 @@ package org.zowe.apiml.zaas.security.service.token; - import com.nimbusds.jose.JOSEException; -import com.nimbusds.jose.jwk.JWK; -import com.nimbusds.jose.jwk.JWKSet; -import com.nimbusds.jose.util.DefaultResourceRetriever; -import com.nimbusds.jose.util.Resource; -import io.jsonwebtoken.*; -import io.jsonwebtoken.lang.Collections; -import io.jsonwebtoken.security.UnsupportedKeyException; +import com.nimbusds.jose.crypto.RSASSAVerifier; +import com.nimbusds.jose.jwk.JWKException; +import com.nimbusds.jose.proc.BadJOSEException; +import com.nimbusds.jwt.JWTClaimsSet; +import com.nimbusds.jwt.JWTParser; +import com.nimbusds.jwt.SignedJWT; import jakarta.annotation.PostConstruct; import lombok.Getter; import lombok.RequiredArgsConstructor; @@ -27,22 +25,26 @@ import org.apache.hc.client5.http.classic.methods.HttpGet; import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; import org.apache.http.HttpHeaders; +import org.jose4j.jwk.JsonWebKey; +import org.jose4j.jwk.JsonWebKeySet; +import org.jose4j.lang.JoseException; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; import org.springframework.http.HttpStatus; import org.springframework.retry.annotation.Retryable; import org.springframework.stereotype.Service; +import org.springframework.util.CollectionUtils; import org.zowe.apiml.constants.ApimlConstants; import org.zowe.apiml.security.common.token.OIDCProvider; import java.io.IOException; -import java.net.URL; -import java.security.Key; +import java.security.interfaces.RSAPublicKey; import java.text.ParseException; +import java.time.Clock; import java.util.List; import java.util.Map; -import java.util.Optional; +import java.util.Map.Entry; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; @@ -53,9 +55,6 @@ @ConditionalOnExpression("'${apiml.security.oidc.enabled:false}' == 'true'") public class OIDCTokenProvider implements OIDCProvider { - private final LocatorAdapterKid keyLocator = new LocatorAdapterKid(); - - @Value("${apiml.security.oidc.jwks.uri}") private List jwksUri; @@ -64,17 +63,16 @@ public class OIDCTokenProvider implements OIDCProvider { @Qualifier("oidcJwtClock") private final Clock clock; - private final DefaultResourceRetriever resourceRetriever; @Value("${apiml.security.oidc.userInfo.uri}") private String endpointUrl; + private final JWKResolver jwkResolver; private final CloseableHttpClient secureHttpClientWithKeystore; @Getter - private final Map publicKeys = new ConcurrentHashMap<>(); + private final Map publicKeys = new ConcurrentHashMap<>(); @Getter - private JWKSet jwkSet; - + private JsonWebKeySet jwkSet; @PostConstruct public void afterPropertiesSet() { @@ -85,7 +83,7 @@ public void afterPropertiesSet() { @Retryable void fetchJWKSet() { - if (Collections.isEmpty(jwksUri)) { + if (jwksUri == null || jwksUri.isEmpty()) { log.debug("OIDC JWK URI not provided, JWK refresh not performed"); return; } @@ -94,32 +92,34 @@ void fetchJWKSet() { for (String url : jwksUri) { log.debug("Refreshing JWK endpoints {}", url); try { - Resource resource = resourceRetriever.retrieveResource(new URL(url)); - var tmpJwk = JWKSet.parse(resource.getContent()); - tmpJwk.getKeys().forEach(jwk -> publicKeys.put(jwk.getKeyID(), jwk)); - } catch (IOException | ParseException | IllegalStateException e) { + var keySet = jwkResolver.resolve(url); + keySet.getJsonWebKeys().forEach(jwk -> publicKeys.put(jwk.getKeyId(), jwk)); + } catch (IOException | IllegalStateException | JoseException e) { log.error("Error processing response from URI {} message: {}", url, e.getMessage()); } } - jwkSet = new JWKSet(publicKeys.values().stream().toList()); + jwkSet = new JsonWebKeySet(publicKeys.entrySet().stream().map(Entry::getValue).toList()); } @Override public boolean isValid(String token) { try { - - if (Collections.isEmpty(jwksUri) || getClaims(token).isEmpty()) { + if (CollectionUtils.isEmpty(jwksUri) || getClaims(token) == null) { return isValidExternal(token); } return true; - } catch (MalformedJwtException jwte) { - log.debug("Malformed JWT: {}", jwte.getMessage(), jwte.getCause()); + } catch (ParseException e) { + log.debug("Malformed JWT: {}", e.getMessage(), e.getCause()); return false; - } catch (JwtException jwte) { - log.debug("JWK token validation failed with the exception {}", jwte.getMessage(), jwte.getCause()); + } catch (JOSEException e) { + log.debug("JWK token validation failed with the exception {}", e.getMessage(), e.getCause()); return isValidExternal(token); + } catch (BadJOSEException e) { + log.debug("Bad JWT: {}", e.getMessage(), e.getCause()); + return false; } + } public boolean isValidExternal(String token) { @@ -141,47 +141,56 @@ public boolean isValidExternal(String token) { log.error("An error occurred during validation of OIDC token using userInfo URI {}: {}", endpointUrl, e.getMessage()); return false; } + } - Claims getClaims(String token) { - if (jwkSet == null || jwkSet.isEmpty()) { + JWTClaimsSet getClaims(String token) throws ParseException, BadJOSEException, JOSEException { + if (StringUtils.isBlank(token)) { + throw new BadJOSEException("Empty string provided instead of a token."); + } + + if (jwkSet == null || jwkSet.getJsonWebKeys().isEmpty()) { fetchJWKSet(); } - if (StringUtils.isBlank(token)) { - throw new JwtException("Empty string provided instead of a token."); + if (jwkSet == null || jwkSet.getJsonWebKeys().isEmpty()) { + throw new JWKException("Could not validate the token due to missing public key."); } + log.debug("Validating the token with JWK"); - return Jwts.parser() - .clock(clock) - .keyLocator(keyLocator) - .build() - .parseSignedClaims(token) - .getPayload(); + var jwt = JWTParser.parse(token); + if (jwt instanceof SignedJWT signedJwt) { + return getClaims(signedJwt); + } else { + log.debug("OIDC Token is not signed"); + } + return null; + } - class LocatorAdapterKid extends LocatorAdapter { + private JWTClaimsSet getClaims(SignedJWT jwt) throws JOSEException, ParseException, BadJOSEException { + var keyId = jwt.getHeader().getKeyID(); + if (StringUtils.isBlank(keyId)) { + throw new JWKException("Token does not provide kid. It uses an unsupported type of signature."); + } - @Override - protected Key locate(ProtectedHeader header) { - if (jwkSet == null || jwkSet.isEmpty()) { - throw new JwtException("Could not validate the token due to missing public key."); + var jsonWebKey = publicKeys.get(keyId); + if (jsonWebKey != null) { + var rsaVerifier = new RSASSAVerifier((RSAPublicKey) jsonWebKey.getKey()); + var verified = jwt.verify(rsaVerifier); + if (verified) { + var claims = jwt.getJWTClaimsSet(); + if (claims.getExpirationTime().toInstant().isBefore(clock.instant())) { + log.debug("OIDC Token is expired"); + return null; + } + return claims; + } else { + throw new BadJOSEException("Provided OIDC JWT token has invalid signature"); } - var kid = header.getKeyId(); - if (kid == null) { - throw new UnsupportedKeyException("Token does not provide kid. It uses an unsupported type of signature."); - } - return Optional.ofNullable(jwkSet.getKeyByKeyId(header.getKeyId())) - .map(key -> { - try { - return key.toRSAKey().toPublicKey(); - } catch (JOSEException e) { - throw new JwtException("Could not validate the token due to either an invalid token or an invalid public key.", e); - } - }) - .orElseThrow(() -> new UnsupportedKeyException("Key with id " + header.getKeyId() + " is null in JWK")); + } else { + throw new JWKException("Key with id " + keyId + " is null in JWK"); } - } } diff --git a/zaas-service/src/main/java/org/zowe/apiml/zaas/security/service/zosmf/ZosmfService.java b/zaas-service/src/main/java/org/zowe/apiml/zaas/security/service/zosmf/ZosmfService.java index afa641ee7d..d6884db8a3 100644 --- a/zaas-service/src/main/java/org/zowe/apiml/zaas/security/service/zosmf/ZosmfService.java +++ b/zaas-service/src/main/java/org/zowe/apiml/zaas/security/service/zosmf/ZosmfService.java @@ -13,9 +13,6 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.databind.ObjectMapper; -import com.nimbusds.jose.jwk.JWKSet; -import com.nimbusds.jose.util.DefaultResourceRetriever; -import com.nimbusds.jose.util.Resource; import jakarta.annotation.PostConstruct; import lombok.AllArgsConstructor; import lombok.Data; @@ -23,6 +20,8 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang.StringUtils; +import org.jose4j.jwk.JsonWebKeySet; +import org.jose4j.lang.JoseException; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.cache.annotation.Cacheable; import org.springframework.context.ApplicationContext; @@ -55,13 +54,13 @@ import org.zowe.apiml.zaas.security.service.AuthenticationService; import org.zowe.apiml.zaas.security.service.TokenCreationService; import org.zowe.apiml.zaas.security.service.schema.source.AuthSource; +import org.zowe.apiml.zaas.security.service.token.JWKResolver; import javax.management.ServiceNotFoundException; - import java.io.IOException; import java.net.MalformedURLException; import java.net.URL; -import java.text.ParseException; +import java.util.Collections; import java.util.EnumMap; import java.util.HashMap; import java.util.List; @@ -125,19 +124,19 @@ public static class ZosmfInfo { } private final List tokenValidationStrategy; - + private final AuthenticationService authenticationService; + private final JWKResolver jwkResolver; private ZosmfService meAsProxy; private TokenCreationService tokenCreationService; - private final DefaultResourceRetriever resourceRetriever; public ZosmfService( - final AuthConfigurationProperties authConfigurationProperties, - final @Qualifier("restTemplateWithoutKeystore") RestTemplate restTemplateWithoutKeystore, - final ObjectMapper securityObjectMapper, - final ApplicationContext applicationContext, - final AuthenticationService authenticationService, - List tokenValidationStrategy, - DefaultResourceRetriever resourceRetriever + final AuthConfigurationProperties authConfigurationProperties, + final @Qualifier("restTemplateWithoutKeystore") RestTemplate restTemplateWithoutKeystore, + final ObjectMapper securityObjectMapper, + final ApplicationContext applicationContext, + final AuthenticationService authenticationService, + List tokenValidationStrategy, + JWKResolver jwkResolver ) { super( applicationContext, @@ -147,11 +146,9 @@ public ZosmfService( ); this.tokenValidationStrategy = tokenValidationStrategy; this.authenticationService = authenticationService; - this.resourceRetriever = resourceRetriever; + this.jwkResolver = jwkResolver; } - private final AuthenticationService authenticationService; - @PostConstruct @Override public void afterPropertiesSet() { @@ -441,11 +438,11 @@ public boolean jwtEndpointExists(HttpHeaders headers) { return false; } else { // other 400 family code - apimlLog.log(JWT_ENDPOINT_ERROR_MSGID, url, hce.getRawStatusCode() + ": " + hce.getMessage()); + apimlLog.log(JWT_ENDPOINT_ERROR_MSGID, url, hce.getStatusCode().value() + ": " + hce.getMessage()); return false; } } catch (HttpServerErrorException serverError) { - apimlLog.log(JWT_ENDPOINT_ERROR_MSGID, url, serverError.getRawStatusCode() + ": " + serverError.getMessage()); + apimlLog.log(JWT_ENDPOINT_ERROR_MSGID, url, serverError.getStatusCode().value() + ": " + serverError.getMessage()); return false; } catch (Exception e) { apimlLog.log(JWT_ENDPOINT_ERROR_MSGID, url, e.getMessage()); @@ -561,19 +558,13 @@ protected ZosmfService.AuthenticationResponse getAuthenticationResponse(Response return new ZosmfService.AuthenticationResponse(tokens); } - public JWKSet getPublicKeys() { - final String url = getURI(getZosmfServiceId(), authConfigurationProperties.getZosmf().getJwtEndpoint()); - + public JsonWebKeySet getPublicKeys() { + var jwkZosmfUrl = getURI(getZosmfServiceId(), authConfigurationProperties.getZosmf().getJwtEndpoint()); try { - Resource resource = resourceRetriever.retrieveResource(new URL(url)); - return JWKSet.parse(resource.getContent()); - } 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 jwkResolver.resolve(jwkZosmfUrl); + } catch (JoseException | IOException e) { + log.debug("Unable to get JWKs from z/OSMF: {}", e.getMessage(), e); + return new JsonWebKeySet(Collections.emptyList()); } - return new JWKSet(); } } diff --git a/zaas-service/src/test/java/org/zowe/apiml/acceptance/ZaasTest.java b/zaas-service/src/test/java/org/zowe/apiml/acceptance/ZaasTest.java index 88a4945737..62b7b883e0 100644 --- a/zaas-service/src/test/java/org/zowe/apiml/acceptance/ZaasTest.java +++ b/zaas-service/src/test/java/org/zowe/apiml/acceptance/ZaasTest.java @@ -20,10 +20,10 @@ import org.springframework.boot.test.web.server.LocalServerPort; import org.springframework.test.context.TestPropertySource; import org.zowe.apiml.product.web.HttpConfig; +import org.zowe.apiml.security.common.util.JWTTestUtils; import org.zowe.apiml.util.config.SslContext; import org.zowe.apiml.util.config.SslContextConfigurer; import org.zowe.apiml.zaas.ZaasApplication; -import org.zowe.apiml.zaas.utils.JWTUtils; import static io.restassured.RestAssured.config; import static io.restassured.RestAssured.given; @@ -74,7 +74,7 @@ void setUp() throws Exception { @Test void givenZosmfCookieAndDummyAuthProvider_whenZoweJwtRequest_thenUnavailable() { - String zosmfJwt = JWTUtils.createZosmfJwtToken("user", "z/OS", "Ltpa", httpConfig.getHttpsConfig()); + String zosmfJwt = JWTTestUtils.createZosmfJwtToken("user", "z/OS", "Ltpa", httpConfig.getHttpsConfig()); //@formatter:off given().config(config().sslConfig(new SSLConfig().sslSocketFactory( diff --git a/zaas-service/src/test/java/org/zowe/apiml/zaas/controllers/AuthControllerTest.java b/zaas-service/src/test/java/org/zowe/apiml/zaas/controllers/AuthControllerTest.java index 82d92d80b2..a88a2f6eb3 100644 --- a/zaas-service/src/test/java/org/zowe/apiml/zaas/controllers/AuthControllerTest.java +++ b/zaas-service/src/test/java/org/zowe/apiml/zaas/controllers/AuthControllerTest.java @@ -11,8 +11,9 @@ package org.zowe.apiml.zaas.controllers; import com.fasterxml.jackson.databind.ObjectMapper; -import com.nimbusds.jose.jwk.JWK; -import com.nimbusds.jose.jwk.JWKSet; +import org.jose4j.jwk.JsonWebKey; +import org.jose4j.jwk.JsonWebKeySet; +import org.jose4j.lang.JoseException; import org.json.JSONException; import org.json.JSONObject; import org.junit.jupiter.api.BeforeEach; @@ -42,18 +43,32 @@ import org.zowe.apiml.zaas.security.webfinger.WebFingerResponse; import java.io.IOException; -import java.text.ParseException; +import java.math.BigInteger; +import java.security.interfaces.RSAPublicKey; import java.util.Arrays; import java.util.Collections; 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 { @@ -81,11 +96,11 @@ class AuthControllerTest { private MessageService messageService; - private JWK zosmfJwk, apimlJwk; + private JsonWebKey zosmfJwk, apimlJwk; private JSONObject body; @BeforeEach - void setUp() throws ParseException, JSONException { + void setUp() throws JSONException, JoseException { messageService = new YamlMessageService("/zaas-log-messages.yml"); authController = new AuthController(authenticationService, jwtSecurity, zosmfService, messageService, tokenProvider, oidcProvider, webFingerProvider); mockMvc = MockMvcBuilders.standaloneSetup(authController).build(); @@ -120,8 +135,8 @@ void distributeInvalidate() throws Exception { this.mockMvc.perform(get("/zaas/api/v1/auth/distribute/instance2")).andExpect(status().is(SC_NO_CONTENT)); } - private JWK getJwk(int i) throws ParseException { - return JWK.parse("{" + + private JsonWebKey getJwk(int i) throws JoseException { + return JsonWebKey.Factory.newJwk("{" + "\"e\":\"AQAB\"," + "\"n\":\"kWp2zRA23Z3vTL4uoe8kTFptxBVFunIoP4t_8TDYJrOb7D1iZNDXVeEsYKp6ppmrTZDAgd-cNOTKLd4M39WJc5FN0maTAVKJc7NxklDeKc4dMe1BGvTZNG4MpWBo-taKULlYUu0ltYJuLzOjIrTHfarucrGoRWqM0sl3z2-fv9k\",\n" + "\"kty\":\"RSA\",\n" + @@ -130,13 +145,13 @@ private JWK getJwk(int i) throws ParseException { } private void initPublicKeys() { - JWKSet zosmf = mock(JWKSet.class); - when(zosmf.getKeys()).thenReturn( + var zosmf = mock(JsonWebKeySet.class); + when(zosmf.getJsonWebKeys()).thenReturn( Collections.singletonList(zosmfJwk) ); when(zosmfService.getPublicKeys()).thenReturn(zosmf); - when(jwtSecurity.getPublicKeyInSet()).thenReturn(new JWKSet(Collections.singletonList(apimlJwk))); + when(jwtSecurity.getPublicKeyInSet()).thenReturn(new JsonWebKeySet(Collections.singletonList(apimlJwk))); when(jwtSecurity.getJwkPublicKey()).thenReturn(Optional.of(apimlJwk)); } @@ -144,34 +159,34 @@ private void initPublicKeys() { void testGetAllPublicKeys() throws Exception { initPublicKeys(); when(jwtSecurity.actualJwtProducer()).thenReturn(JwtSecurity.JwtProducer.ZOSMF); - JWKSet jwkSet = new JWKSet(Arrays.asList(zosmfJwk, apimlJwk)); + var jwkSet = new JsonWebKeySet(Arrays.asList(zosmfJwk, apimlJwk)); this.mockMvc.perform(get("/zaas/api/v1/auth/keys/public/all")) .andExpect(status().is(SC_OK)) - .andExpect(content().json(jwkSet.toString())); + .andExpect(content().json(jwkSet.toJson())); } @Test void givenAPIMLJWTProducer_whenGetAllPublicKeys_thenReturnsOnlyAPIMLKeys() throws Exception { initPublicKeys(); when(jwtSecurity.actualJwtProducer()).thenReturn(JwtSecurity.JwtProducer.APIML); - JWKSet jwkSet = new JWKSet(Collections.singletonList(apimlJwk)); + var jwkSet = new JsonWebKeySet(Collections.singletonList(apimlJwk)); this.mockMvc.perform(get("/zaas/api/v1/auth/keys/public/all")) .andExpect(status().is(SC_OK)) - .andExpect(content().json(jwkSet.toString())); + .andExpect(content().json(jwkSet.toJson())); } @Test void givenOIDCJWKSet_whenGetAllPublicKeys_thenIncludeOIDCInResult() throws Exception { initPublicKeys(); - JWKSet mockedJwkSet = mock(JWKSet.class); - JWK oidcJwk = getJwk(3); + var mockedJwkSet = mock(JsonWebKeySet.class); + var oidcJwk = getJwk(3); when(oidcProvider.getJwkSet()).thenReturn(mockedJwkSet); - when(mockedJwkSet.getKeys()).thenReturn(Collections.singletonList(oidcJwk)); + when(mockedJwkSet.getJsonWebKeys()).thenReturn(Collections.singletonList(oidcJwk)); - JWKSet jwkSet = new JWKSet(Arrays.asList(apimlJwk, oidcJwk)); + var jwkSet = new JsonWebKeySet(Arrays.asList(apimlJwk, oidcJwk)); this.mockMvc.perform(get("/zaas/api/v1/auth/keys/public/all")) .andExpect(status().is(SC_OK)) - .andExpect(content().json(jwkSet.toString())); + .andExpect(content().json(jwkSet.toJson())); } @Nested @@ -180,30 +195,30 @@ class WhenGettingActiveKey { void useZoweJwt() throws Exception { initPublicKeys(); when(jwtSecurity.actualJwtProducer()).thenReturn(JwtSecurity.JwtProducer.APIML); - JWKSet jwkSet = new JWKSet(Collections.singletonList(apimlJwk)); + var jwkSet = new JsonWebKeySet(Collections.singletonList(apimlJwk)); mockMvc.perform(get("/zaas/api/v1/auth/keys/public/current")) .andExpect(status().is(SC_OK)) - .andExpect(content().json(jwkSet.toString())); + .andExpect(content().json(jwkSet.toJson())); } @Test void returnEmptyWhenUnknown() throws Exception { initPublicKeys(); when(jwtSecurity.actualJwtProducer()).thenReturn(JwtSecurity.JwtProducer.UNKNOWN); - JWKSet jwkSet = new JWKSet(Collections.emptyList()); + var jwkSet = new JsonWebKeySet(Collections.emptyList()); mockMvc.perform(get("/zaas/api/v1/auth/keys/public/current")) .andExpect(status().is(SC_OK)) - .andExpect(content().json(jwkSet.toString())); + .andExpect(content().json(jwkSet.toJson())); } @Test void useZosmf() throws Exception { initPublicKeys(); - JWKSet jwkSet = new JWKSet(Collections.singletonList(zosmfJwk)); + var jwkSet = new JsonWebKeySet(Collections.singletonList(zosmfJwk)); when(jwtSecurity.actualJwtProducer()).thenReturn(JwtSecurity.JwtProducer.ZOSMF); mockMvc.perform(get("/zaas/api/v1/auth/keys/public/current")) .andExpect(status().is(SC_OK)) - .andExpect(content().json(jwkSet.toString())); + .andExpect(content().json(jwkSet.toJson())); } } @@ -214,7 +229,7 @@ class GivenZosmfIsProducer { @Test void whenOnlineAndSupportJwt_returnValidPemKey() throws Exception { when(jwtSecurity.actualJwtProducer()).thenReturn(JwtSecurity.JwtProducer.ZOSMF); - when(zosmfService.getPublicKeys()).thenReturn(new JWKSet(getJwk(0))); + when(zosmfService.getPublicKeys()).thenReturn(new JsonWebKeySet(getJwk(0))); mockMvc.perform(get("/zaas/api/v1/auth/keys/public")) .andExpect(status().is(SC_OK)); @@ -223,19 +238,18 @@ void whenOnlineAndSupportJwt_returnValidPemKey() throws Exception { @Test void whenToPublicKeyThrowsException_thenReturnsInternalServerError() throws Exception { byte[] badModulus = new byte[]{0}; - com.nimbusds.jose.jwk.RSAKey badKey = new com.nimbusds.jose.jwk.RSAKey.Builder( - new java.security.interfaces.RSAPublicKey() { - public java.math.BigInteger getModulus() { return new java.math.BigInteger(badModulus); } - public java.math.BigInteger getPublicExponent() { return java.math.BigInteger.ONE; } - public String getAlgorithm() { return "RSA"; } - public String getFormat() { return null; } - public byte[] getEncoded() { return new byte[0]; } - }) - .keyID("broken") - .build(); + + var badKey = mock(RSAPublicKey.class); + when(badKey.getModulus()).thenReturn(new BigInteger(badModulus)); + when(badKey.getPublicExponent()).thenReturn(BigInteger.ONE); + when(badKey.getAlgorithm()).thenReturn("RSA"); + when(badKey.getFormat()).thenReturn(null); + when(badKey.getEncoded()).thenReturn(new byte[0]); + + var badJwk = JsonWebKey.Factory.newJwk(badKey); when(jwtSecurity.actualJwtProducer()).thenReturn(JwtSecurity.JwtProducer.APIML); - when(jwtSecurity.getPublicKeyInSet()).thenReturn(new JWKSet(List.of(badKey))); + when(jwtSecurity.getPublicKeyInSet()).thenReturn(new JsonWebKeySet(List.of(badJwk))); mockMvc.perform(get("/zaas/api/v1/auth/keys/public")) .andExpect(status().isInternalServerError()) @@ -253,9 +267,9 @@ void whenNotReady_returnInternalServerError() throws Exception { @Test void whenZosmfReturnsIncorrectAmountOfKeys_returnInternalServerError() throws Exception { - List jwkList = Arrays.asList(mock(JWK.class), mock(JWK.class)); + var jwkList = Arrays.asList(mock(JsonWebKey.class), mock(JsonWebKey.class)); when(jwtSecurity.actualJwtProducer()).thenReturn(JwtSecurity.JwtProducer.ZOSMF); - when(zosmfService.getPublicKeys()).thenReturn(new JWKSet(jwkList)); + when(zosmfService.getPublicKeys()).thenReturn(new JsonWebKeySet(jwkList)); mockMvc.perform(get("/zaas/api/v1/auth/keys/public")) .andExpect(status().is(SC_INTERNAL_SERVER_ERROR)) @@ -268,7 +282,7 @@ class GivenApiMlIsProducer { @Test void returnValidPemKey() throws Exception { when(jwtSecurity.actualJwtProducer()).thenReturn(JwtSecurity.JwtProducer.APIML); - when(jwtSecurity.getPublicKeyInSet()).thenReturn(new JWKSet(getJwk(0))); + when(jwtSecurity.getPublicKeyInSet()).thenReturn(new JsonWebKeySet(getJwk(0))); mockMvc.perform(get("/zaas/api/v1/auth/keys/public")) .andExpect(status().is(SC_OK)); diff --git a/zaas-service/src/test/java/org/zowe/apiml/zaas/security/query/SuccessfulQueryHandlerTest.java b/zaas-service/src/test/java/org/zowe/apiml/zaas/security/query/SuccessfulQueryHandlerTest.java index 03875e7d5b..e956fd6470 100644 --- a/zaas-service/src/test/java/org/zowe/apiml/zaas/security/query/SuccessfulQueryHandlerTest.java +++ b/zaas-service/src/test/java/org/zowe/apiml/zaas/security/query/SuccessfulQueryHandlerTest.java @@ -12,8 +12,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.netflix.discovery.EurekaClient; -import io.jsonwebtoken.Jwts; -import io.jsonwebtoken.security.SignatureAlgorithm; +import com.nimbusds.jose.JWSAlgorithm; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -39,9 +38,14 @@ import java.security.KeyPair; import java.security.PrivateKey; +import java.time.Clock; import java.util.ArrayList; -import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.lenient; import static org.mockito.Mockito.when; @ExtendWith(MockitoExtension.class) @@ -79,13 +83,16 @@ class SuccessfulQueryHandlerTest { @Mock private TokenCreationService tokenCreationService; + @Mock + private Clock clock; + @BeforeEach void setup() { httpServletRequest = new MockHttpServletRequest(); httpServletResponse = new MockHttpServletResponse(); AuthConfigurationProperties authConfigurationProperties = new AuthConfigurationProperties(); - SignatureAlgorithm algorithm = Jwts.SIG.RS256; + var algorithm = JWSAlgorithm.RS256; KeyPair keyPair = SecurityUtils.generateKeyPair("RSA", 2048); PrivateKey privateKey = null; if (keyPair != null) { @@ -104,9 +111,9 @@ void setup() { AuthenticationService authService = new AuthenticationService( applicationContext, authConfigurationProperties, jwtSecurityInitializer, zosmfService, - eurekaClient, restTemplate, cacheManager, new CacheUtils() + eurekaClient, restTemplate, cacheManager, new CacheUtils(), clock ); - when(jwtSecurityInitializer.getSignatureAlgorithm()).thenReturn(algorithm); + lenient().when(jwtSecurityInitializer.getSignatureAlgorithm()).thenReturn(algorithm); when(jwtSecurityInitializer.getJwtSecret()).thenReturn(privateKey); jwtToken = authService.createJwtToken(USER, DOMAIN, LTPA); diff --git a/zaas-service/src/test/java/org/zowe/apiml/zaas/security/service/AuthenticationServiceTest.java b/zaas-service/src/test/java/org/zowe/apiml/zaas/security/service/AuthenticationServiceTest.java index 4793c5400e..35137e4b55 100644 --- a/zaas-service/src/test/java/org/zowe/apiml/zaas/security/service/AuthenticationServiceTest.java +++ b/zaas-service/src/test/java/org/zowe/apiml/zaas/security/service/AuthenticationServiceTest.java @@ -14,10 +14,10 @@ import com.netflix.appinfo.InstanceInfo; import com.netflix.discovery.EurekaClient; import com.netflix.discovery.shared.Application; +import com.nimbusds.jose.JWSAlgorithm; +import com.nimbusds.jwt.JWTClaimsSet; import io.jsonwebtoken.Claims; import io.jsonwebtoken.Jwts; -import io.jsonwebtoken.impl.DefaultClaims; -import io.jsonwebtoken.security.SignatureAlgorithm; import jakarta.servlet.http.Cookie; import org.apache.commons.lang.time.DateUtils; import org.junit.jupiter.api.BeforeEach; @@ -48,6 +48,7 @@ import org.zowe.apiml.security.common.token.TokenAuthentication; import org.zowe.apiml.security.common.token.TokenExpireException; import org.zowe.apiml.security.common.token.TokenNotValidException; +import org.zowe.apiml.security.common.util.JwtUtils; import org.zowe.apiml.util.CacheUtils; import org.zowe.apiml.util.EurekaUtils; import org.zowe.apiml.zaas.config.CacheConfig; @@ -57,11 +58,33 @@ import java.security.KeyPair; import java.security.PrivateKey; import java.security.PublicKey; -import java.util.*; +import java.text.ParseException; +import java.time.Clock; +import java.time.Instant; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Date; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; import java.util.function.Consumer; -import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.Mockito.*; +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.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.lenient; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; @ExtendWith(MockitoExtension.class) public class AuthenticationServiceTest { //NOSONAR, needs to be public @@ -72,7 +95,7 @@ public class AuthenticationServiceTest { //NOSONAR, needs to be public private Set scopes; private static final String DOMAIN = "this.com"; private static final String LTPA = "ltpaToken"; - private static final SignatureAlgorithm ALGORITHM = Jwts.SIG.RS256; + private static final JWSAlgorithm ALGORITHM = JWSAlgorithm.RS256; private static PrivateKey privateKey; private static PublicKey publicKey; @@ -97,6 +120,8 @@ public class AuthenticationServiceTest { //NOSONAR, needs to be public private CacheUtils cacheUtils; @Mock private CacheManager cacheManager; + @Mock + private Clock clock; static { @@ -109,18 +134,18 @@ public class AuthenticationServiceTest { //NOSONAR, needs to be public @BeforeEach void setup() { - authConfigurationProperties = new AuthConfigurationProperties(); authConfigurationProperties.getZosmf().setServiceId(ZOSMF); authService = new AuthenticationService( applicationContext, authConfigurationProperties, jwtSecurityInitializer, - zosmfService, eurekaClient, restTemplate, cacheManager, cacheUtils + zosmfService, eurekaClient, restTemplate, cacheManager, cacheUtils, clock ); scopes = new HashSet<>(); scopes.add("Service1"); scopes.add("Service2"); ReflectionTestUtils.setField(authService, "meAsProxy", authService); + lenient().when(clock.instant()).thenReturn(Instant.now()); } @Nested @@ -360,8 +385,9 @@ void givenInvalidJWT_thenThrowTokenNotValidException() { @Test void givenExpiredJWT_thenThrowTokenExpireException() { - String expiredJwtToken = createExpiredJwtToken(privateKey); + var expiredJwtToken = createExpiredJwtToken(privateKey); when(jwtSecurityInitializer.getJwtPublicKey()).thenReturn(publicKey); + when(clock.instant()).thenReturn(Instant.now()); assertThrows( TokenExpireException.class, () -> authService.getLtpaTokenWithValidation(expiredJwtToken) @@ -386,7 +412,7 @@ private String createJwtTokenWithExpiry(PrivateKey privateKey, long expireAt) { return Jwts.builder() .setExpiration(new Date(expireAt)) .setIssuer(authConfigurationProperties.getTokenProperties().getIssuer()) - .signWith(privateKey, ALGORITHM) + .signWith(privateKey, Jwts.SIG.RS256) .compact(); } @@ -495,10 +521,10 @@ class GivenTokenOriginTest { private static final String TOKEN = "some_token"; @Test - void thenReturnCorrectOrigin() { - final Map map = new HashMap<>(); + void thenReturnCorrectOrigin() throws ParseException { + final Map map = new HashMap<>(); map.put(Claims.ISSUER, "APIML_PAT"); - final Claims tokenClaims = new DefaultClaims(map); + var tokenClaims = JWTClaimsSet.parse(map); AuthSource.Origin originResult; try (MockedStatic jwtUtilsMock = Mockito.mockStatic(JwtUtils.class)) { @@ -531,7 +557,7 @@ void stubJWTSecurityForSignAndVerify() { } void stubJWTSecurityForSign() { - when(jwtSecurityInitializer.getSignatureAlgorithm()).thenReturn(ALGORITHM); + lenient().when(jwtSecurityInitializer.getSignatureAlgorithm()).thenReturn(ALGORITHM); when(jwtSecurityInitializer.getJwtSecret()).thenReturn(privateKey); } @@ -555,6 +581,9 @@ class GivenCacheJWTTest { @MockitoBean(name = "restTemplateWithKeystore") private RestTemplate restTemplateWithKeystore; + @MockitoBean + private Clock clock; + @Autowired private AuthenticationService authService; @@ -566,6 +595,7 @@ void thenUseCache() { when(jwtSecurityInitializer.getSignatureAlgorithm()).thenReturn(ALGORITHM); when(jwtSecurityInitializer.getJwtSecret()).thenReturn(privateKey); when(jwtSecurityInitializer.getJwtPublicKey()).thenReturn(publicKey); + when(clock.instant()).thenReturn(Instant.now()); String jwtToken01 = authService.createJwtToken("user01", "domain01", "ltpa01"); String jwtToken02 = authService.createJwtToken("user02", "domain02", "ltpa02"); diff --git a/zaas-service/src/test/java/org/zowe/apiml/zaas/security/service/JwtSecurityTest.java b/zaas-service/src/test/java/org/zowe/apiml/zaas/security/service/JwtSecurityTest.java index b5bff8960b..8f2421db95 100644 --- a/zaas-service/src/test/java/org/zowe/apiml/zaas/security/service/JwtSecurityTest.java +++ b/zaas-service/src/test/java/org/zowe/apiml/zaas/security/service/JwtSecurityTest.java @@ -14,8 +14,6 @@ import com.netflix.discovery.EurekaClient; import com.netflix.discovery.EurekaEventListener; import com.netflix.discovery.StatusChangeEvent; -import com.nimbusds.jose.jwk.JWK; -import com.nimbusds.jose.jwk.JWKSet; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; @@ -25,13 +23,18 @@ import org.zowe.apiml.security.HttpsConfigError; import org.zowe.apiml.zaas.security.login.Providers; -import java.util.Optional; - -import static org.hamcrest.CoreMatchers.*; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.not; +import static org.hamcrest.CoreMatchers.nullValue; import static org.hamcrest.MatcherAssert.assertThat; -import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.Mockito.any; -import static org.mockito.Mockito.*; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; @ExtendWith(SpringExtension.class) class JwtSecurityTest { @@ -200,22 +203,22 @@ void setUp() { @Test void asSet() { underTest.loadAppropriateJwtKeyOrFail(); - JWKSet result = underTest.getPublicKeyInSet(); + var result = underTest.getPublicKeyInSet(); - assertThat(result.getKeys().size(), is(1)); + assertThat(result.getJsonWebKeys().size(), is(1)); } @Test void whenOnePresent_asOneKey() { underTest.loadAppropriateJwtKeyOrFail(); - Optional result = underTest.getJwkPublicKey(); + var result = underTest.getJwkPublicKey(); assertThat(result.isPresent(), is(true)); } @Test void whenKeyNotLoaded_Empty() { - Optional result = underTest.getJwkPublicKey(); + var result = underTest.getJwkPublicKey(); assertThat(result.isPresent(), is(false)); } diff --git a/zaas-service/src/test/java/org/zowe/apiml/zaas/security/service/schema/source/OIDCAuthSourceServiceTest.java b/zaas-service/src/test/java/org/zowe/apiml/zaas/security/service/schema/source/OIDCAuthSourceServiceTest.java index 41d9b0faee..359c41bc18 100644 --- a/zaas-service/src/test/java/org/zowe/apiml/zaas/security/service/schema/source/OIDCAuthSourceServiceTest.java +++ b/zaas-service/src/test/java/org/zowe/apiml/zaas/security/service/schema/source/OIDCAuthSourceServiceTest.java @@ -44,7 +44,7 @@ import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; -import static org.zowe.apiml.zaas.utils.JWTUtils.createTokenWithUserFields; +import static org.zowe.apiml.security.common.util.JWTTestUtils.createTokenWithUserFields; @ExtendWith(MockitoExtension.class) class OIDCAuthSourceServiceTest { diff --git a/zaas-service/src/test/java/org/zowe/apiml/zaas/security/service/token/JWKResolverTest.java b/zaas-service/src/test/java/org/zowe/apiml/zaas/security/service/token/JWKResolverTest.java new file mode 100644 index 0000000000..035568e86c --- /dev/null +++ b/zaas-service/src/test/java/org/zowe/apiml/zaas/security/service/token/JWKResolverTest.java @@ -0,0 +1,105 @@ +/* + * 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.zaas.security.service.token; + +import org.jose4j.http.Response; +import org.jose4j.http.SimpleGet; +import org.jose4j.jwk.HttpsJwks; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.io.IOException; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class JWKResolverTest { + + @Mock + private HttpsJwks httpsJwks; + + @Mock + private HttpsJwksProvider provider; + + @Mock + private SimpleGet simpleGet; + + private JWKResolver jwkResolver; + + @BeforeEach + void setUp() { + this.jwkResolver = new JWKResolver(provider); + } + + @Nested + class JwksUriLoad { + + @Test + void givenMissingParameterInJWK_doNotThrowException() throws IOException { + var url = "https://localhost/jwk"; + var jwks = new HttpsJwks(url); + jwks.setSimpleHttpGet(simpleGet); + + var json = """ + { + "keys": [ + { + "kty": null, + "alg": "RS256", + "kid": "Lcxckkor94qkrunxHP7Tkib547rzmkXvsYV-nc6U-N4", + "use": "sig", + "e": "AQAB", + "n": "v6wT5k7uLto_VPTV8fW9_wRqWHuqnZbyEYAwNYRdffe9WowwnzUAr0Z93-4xDvCRuVfTfvCe9orEWdjZMaYlDq_Dj5BhLAqmBAF299Kv1GymOioLRDvoVWy0aVHYXXNaqJCPsaWIDiCly-_kJBbnda_rmB28a_878TNxom0mDQ20TI5SgdebqqMBOdHEqIYH1ER9euybekeqJX24EqE9YW4Yug5BOkZ9KcUkiEsH_NPyRlozihj18Qab181PRyKHE6M40W7w67XcRq2llTy-z9RrQupcyvLD7L62KN0ey8luKWnVg4uIOldpyBYyiRX2WPM-2K00RVC0e4jQKs34Gw" + } + ] + } + """; + + when(provider.getFor(url)).thenReturn(jwks); + when(simpleGet.get(url)).thenReturn(new Response(200, "", Map.of(), json)); + + assertDoesNotThrow(() -> jwkResolver.resolve(url)); + } + + @Test + void giveValidJWK_setPublicKey() throws IOException { + var url = "https://localhost/jwk"; + var jwks = new HttpsJwks(url); + jwks.setSimpleHttpGet(simpleGet); + + var json = """ + { + "keys": [ + { + "kty": "RSA", + "alg": "RS256", + "kid": "-716sp3XBB_v30lGj2mu5MdXkdh8poa9zJQlAwC46n4", + "use": "sig", + "e": "AQAB", + "n": "5rYyqFsxel0Pv-xRDHPbg3IfumE4ks9ffLvJrfZVgrTQyiFmFfBnyD3r7y6626Yr5-68Pj0I5SHlCBPkkgTU_e9Z3tCYiegtIOeJdSdumWR2JDVAsbpwFJDG_kxP9czgX7HL0T2BPSapx7ba0ZBXd2-SfSDDL-c1Q0rJ1uQEJwDXAGZV4qy_oXuQf5DuV65Xj8y2Qn1DtVEBThxita-kis_H35CTWgW2zyyaS_08wa00R98mnQ2SHfmO5fZABITmH0DO0coDHqKZ429VNNpELLX9e95dirQ1jfngDbBCmy-XsT8yc6NpAaXmd8P2NHdsO2oK46EQEaFRyMcoDTs3-w" + } + ] + } + """; + when(provider.getFor(url)).thenReturn(jwks); + when(simpleGet.get(url)).thenReturn(new Response(200, "", Map.of(), json)); + + assertDoesNotThrow(() -> jwkResolver.resolve(url)); + } + + } +} diff --git a/zaas-service/src/test/java/org/zowe/apiml/zaas/security/service/token/OIDCTokenProviderTest.java b/zaas-service/src/test/java/org/zowe/apiml/zaas/security/service/token/OIDCTokenProviderTest.java index 6d00752d91..008f0c42b0 100644 --- a/zaas-service/src/test/java/org/zowe/apiml/zaas/security/service/token/OIDCTokenProviderTest.java +++ b/zaas-service/src/test/java/org/zowe/apiml/zaas/security/service/token/OIDCTokenProviderTest.java @@ -10,17 +10,14 @@ package org.zowe.apiml.zaas.security.service.token; -import com.google.common.io.Resources; -import com.nimbusds.jose.jwk.JWKSet; -import com.nimbusds.jose.util.DefaultResourceRetriever; -import com.nimbusds.jose.util.Resource; import io.jsonwebtoken.Jwts; -import io.jsonwebtoken.impl.DefaultClock; import org.apache.hc.client5.http.classic.methods.HttpGet; import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; import org.apache.hc.core5.http.ClassicHttpResponse; import org.apache.hc.core5.http.io.HttpClientResponseHandler; import org.apache.http.HttpHeaders; +import org.jose4j.jwk.JsonWebKeySet; +import org.jose4j.lang.JoseException; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Nested; @@ -35,13 +32,11 @@ import org.zowe.apiml.zaas.cache.CachingServiceClientException; import java.io.IOException; -import java.net.URL; -import java.nio.charset.StandardCharsets; +import java.time.Clock; import java.time.Instant; import java.util.Arrays; import java.util.Collections; import java.util.Date; -import java.util.List; import java.util.UUID; import java.util.stream.Stream; @@ -49,31 +44,28 @@ import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.eq; +import static org.mockito.Mockito.lenient; 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.zowe.apiml.zaas.utils.JWTUtils.loadPrivateKey; +import static org.zowe.apiml.security.common.util.JWTTestUtils.loadPrivateKey; @ExtendWith(MockitoExtension.class) class OIDCTokenProviderTest { - private static final String OKTA_JWKS_RESOURCE = "test_samples/okta_jwks.json"; - private static final String EXPIRED_TOKEN = "eyJraWQiOiJMY3hja2tvcjk0cWtydW54SFA3VGtpYjU0N3J6bWtYdnNZVi1uYzZVLU40IiwiYWxnIjoiUlMyNTYifQ.eyJ2ZXIiOjEsImp0aSI6IkFULlExakp2UkZ0dUhFUFpGTXNmM3A0enQ5aHBRRHZrSU1CQ3RneU9IcTdlaEkiLCJpc3MiOiJodHRwczovL2Rldi05NTcyNzY4Ni5va3RhLmNvbS9vYXV0aDIvZGVmYXVsdCIsImF1ZCI6ImFwaTovL2RlZmF1bHQiLCJpYXQiOjE2OTcwNjA3NzMsImV4cCI6MTY5NzA2NDM3MywiY2lkIjoiMG9hNmE0OG1uaVhBcUVNcng1ZDciLCJ1aWQiOiIwMHU5OTExOGgxNmtQT1dBbTVkNyIsInNjcCI6WyJvcGVuaWQiXSwiYXV0aF90aW1lIjoxNjk3MDYwMDY0LCJzdWIiOiJzajg5NTA5MkBicm9hZGNvbS5uZXQiLCJncm91cHMiOlsiRXZlcnlvbmUiXX0.Cuf1JVq_NnfBxaCwiLsR5O6DBmVV1fj9utAfKWIF1hlek2hCJsDLQM4ii_ucQ0MM1V3nVE1ZatPB-W7ImWPlGz7NeNBv7jEV9DkX70hchCjPHyYpaUhAieTG75obdufiFpI55bz3qH5cPRvsKv0OKKI9T8D7GjEWsOhv6CevJJZZvgCFLGFfnacKLOY5fEBN82bdmCulNfPVrXF23rOregFjOBJ1cKWfjmB0UGWgI8VBGGemMNm3ACX3OYpTOek2PBfoCIZWOSGnLZumFTYA0F_3DsWYhIJNoFv16_EBBJcp_C0BYE_fiuXzeB0fieNUXASsKp591XJMflDQS_Zt1g"; + private static final String MALFORMED_TOKEN = "token"; private static String VALID_TOKEN; - private static final String MALFORMED_TOKEN = "token"; - private static JWKSet localJwkSet; - private static String oktaJwks; + private static JsonWebKeySet localJwkSet; private OIDCTokenProvider oidcTokenProvider; - @Mock - private DefaultResourceRetriever resourceRetriever; @Mock private CloseableHttpClient httpClient; + @Mock + private JWKResolver jwkResolver; static Stream invalidTokens() { return Stream.of( @@ -87,7 +79,7 @@ static void init() throws Exception { var jwkAndSet = loadPrivateKey("../keystore/localhost/localhost.keystore.p12", "localhost", "password"); localJwkSet = jwkAndSet.jwkSet(); VALID_TOKEN = Jwts.builder() - .header().keyId("0987").and() + .header().keyId("Lcxckkor94qkrunxHP7Tkib547rzmkXvsYV-nc6U-N4").and() .subject("user") .issuedAt(Date.from(now)) .expiration(Date.from(now.plusSeconds(1200))) @@ -97,10 +89,9 @@ static void init() throws Exception { } @BeforeEach - void setup() throws CachingServiceClientException, IOException { - oidcTokenProvider = new OIDCTokenProvider(new DefaultClock(), resourceRetriever, httpClient); + void setup() throws CachingServiceClientException { + oidcTokenProvider = new OIDCTokenProvider(Clock.systemUTC(), jwkResolver, httpClient); ReflectionTestUtils.setField(oidcTokenProvider, "jwkRefreshInterval", 1); - oktaJwks = Resources.toString(Resources.getResource(OKTA_JWKS_RESOURCE), StandardCharsets.UTF_8); } @Nested @@ -110,32 +101,40 @@ class GivenInitializationWithJwks { void whenUriNotProvided_thenNotInitialized() throws Exception { ReflectionTestUtils.setField(oidcTokenProvider, "jwksUri", Collections.emptyList()); oidcTokenProvider.afterPropertiesSet(); - verify(resourceRetriever, times(0)).retrieveResource(any()); + verify(jwkResolver, times(0)).resolve(any()); } + + @Test + void shouldNotModifyJwksUri() { + assertDoesNotThrow(() -> oidcTokenProvider.fetchJWKSet()); + assertTrue(oidcTokenProvider.getPublicKeys().isEmpty()); + } + } @Nested class GivenCorrectConfiguration { - @Nested class WhenJWKValidation { @BeforeEach - void init() throws Exception { + void init() { ReflectionTestUtils.setField(oidcTokenProvider, "jwksUri", Arrays.asList("https://localjwk", "https://jwksurl")); - when(resourceRetriever.retrieveResource(eq(new URL("https://jwksurl")))).thenReturn(new Resource(oktaJwks, null)); - when(resourceRetriever.retrieveResource(eq(new URL("https://localjwk")))).thenReturn(new Resource(localJwkSet.toString(), null)); } @ParameterizedTest(name = "#{index} return invalid when given invalid token: {0}") @MethodSource("org.zowe.apiml.zaas.security.service.token.OIDCTokenProviderTest#invalidTokens") - void whenInvalidToken_thenReturnInvalid(String token) { + void whenInvalidToken_thenReturnInvalid(String token) throws JoseException, IOException { + lenient().when(jwkResolver.resolve("https://localjwk")).thenReturn(localJwkSet); + lenient().when(jwkResolver.resolve("https://jwksurl")).thenReturn(localJwkSet); assertFalse(oidcTokenProvider.isValid(token)); } @Test - void whenValidToken_thenReturnValid() { + void whenValidToken_thenReturnValid() throws JoseException, IOException { + when(jwkResolver.resolve("https://localjwk")).thenReturn(localJwkSet); + when(jwkResolver.resolve("https://jwksurl")).thenReturn(localJwkSet); assertTrue(oidcTokenProvider.isValid(VALID_TOKEN)); } @@ -180,73 +179,4 @@ void whenValidToken_thenReturnValid() { } } - - @Nested - class JwksUriLoad { - - @BeforeEach - public void setUp() { - oidcTokenProvider = new OIDCTokenProvider(new DefaultClock(), resourceRetriever, httpClient); - ReflectionTestUtils.setField(oidcTokenProvider, "jwksUri", List.of("https://jwksurl")); - ReflectionTestUtils.setField(oidcTokenProvider, "resourceRetriever", resourceRetriever); - } - - @Test - void shouldNotModifyJwksUri() throws IOException { - var json = "{}"; - - when(resourceRetriever.retrieveResource(any())).thenReturn(new Resource(json, null)); - - assertDoesNotThrow(() -> oidcTokenProvider.fetchJWKSet()); - assertTrue(oidcTokenProvider.getPublicKeys().isEmpty()); - } - - - @Test - void givenMissingParameterInJWK_doNotThrowException() throws IOException { - var json = """ - { - "keys": [ - { - "kty": null, - "alg": "RS256", - "kid": "Lcxckkor94qkrunxHP7Tkib547rzmkXvsYV-nc6U-N4", - "use": "sig", - "e": "AQAB", - "n": "v6wT5k7uLto_VPTV8fW9_wRqWHuqnZbyEYAwNYRdffe9WowwnzUAr0Z93-4xDvCRuVfTfvCe9orEWdjZMaYlDq_Dj5BhLAqmBAF299Kv1GymOioLRDvoVWy0aVHYXXNaqJCPsaWIDiCly-_kJBbnda_rmB28a_878TNxom0mDQ20TI5SgdebqqMBOdHEqIYH1ER9euybekeqJX24EqE9YW4Yug5BOkZ9KcUkiEsH_NPyRlozihj18Qab181PRyKHE6M40W7w67XcRq2llTy-z9RrQupcyvLD7L62KN0ey8luKWnVg4uIOldpyBYyiRX2WPM-2K00RVC0e4jQKs34Gw" - } - ] - } - """; - - when(resourceRetriever.retrieveResource(any())).thenReturn(new Resource(json, null)); - - assertDoesNotThrow(() -> oidcTokenProvider.fetchJWKSet()); - assertTrue(oidcTokenProvider.getPublicKeys().isEmpty()); - } - - @Test - void giveValidJWK_setPublicKey() throws IOException { - var json = """ - { - "keys": [ - { - "kty": "RSA", - "alg": "RS256", - "kid": "-716sp3XBB_v30lGj2mu5MdXkdh8poa9zJQlAwC46n4", - "use": "sig", - "e": "AQAB", - "n": "5rYyqFsxel0Pv-xRDHPbg3IfumE4ks9ffLvJrfZVgrTQyiFmFfBnyD3r7y6626Yr5-68Pj0I5SHlCBPkkgTU_e9Z3tCYiegtIOeJdSdumWR2JDVAsbpwFJDG_kxP9czgX7HL0T2BPSapx7ba0ZBXd2-SfSDDL-c1Q0rJ1uQEJwDXAGZV4qy_oXuQf5DuV65Xj8y2Qn1DtVEBThxita-kis_H35CTWgW2zyyaS_08wa00R98mnQ2SHfmO5fZABITmH0DO0coDHqKZ429VNNpELLX9e95dirQ1jfngDbBCmy-XsT8yc6NpAaXmd8P2NHdsO2oK46EQEaFRyMcoDTs3-w" - } - ] - } - """; - - when(resourceRetriever.retrieveResource(any())).thenReturn(new Resource(json, null)); - - oidcTokenProvider.fetchJWKSet(); - assertFalse(oidcTokenProvider.getPublicKeys().isEmpty()); - } - - } } diff --git a/zaas-service/src/test/java/org/zowe/apiml/zaas/security/service/zosmf/ZosmfServiceTest.java b/zaas-service/src/test/java/org/zowe/apiml/zaas/security/service/zosmf/ZosmfServiceTest.java index a4114cee4c..17c9828fef 100644 --- a/zaas-service/src/test/java/org/zowe/apiml/zaas/security/service/zosmf/ZosmfServiceTest.java +++ b/zaas-service/src/test/java/org/zowe/apiml/zaas/security/service/zosmf/ZosmfServiceTest.java @@ -16,9 +16,9 @@ import ch.qos.logback.classic.spi.LoggingEvent; import ch.qos.logback.core.Appender; import com.fasterxml.jackson.databind.ObjectMapper; -import com.nimbusds.jose.util.DefaultResourceRetriever; -import com.nimbusds.jose.util.Resource; import org.hamcrest.collection.IsMapContaining; +import org.jose4j.jwk.JsonWebKeySet; +import org.jose4j.lang.JoseException; import org.json.JSONException; import org.json.JSONObject; import org.junit.jupiter.api.AfterEach; @@ -60,6 +60,7 @@ import org.zowe.apiml.zaas.security.service.TokenCreationService; import org.zowe.apiml.zaas.security.service.schema.source.AuthSource; import org.zowe.apiml.zaas.security.service.schema.source.ParsedTokenAuthSource; +import org.zowe.apiml.zaas.security.service.token.JWKResolver; import javax.management.ServiceNotFoundException; import javax.net.ssl.SSLHandshakeException; @@ -108,24 +109,20 @@ class ZosmfServiceTest { private static final String ZOSMF_ID = "zosmf"; private final AuthConfigurationProperties authConfigurationProperties = mock(AuthConfigurationProperties.class); - @Mock private RestTemplate restTemplate; - @Mock private ApplicationContext applicationContext; - @Mock private TokenValidationStrategy tokenValidationStrategy1; - @Mock private TokenValidationStrategy tokenValidationStrategy2; - @Mock private AuthenticationService authenticationService; - @Mock private TokenCreationService tokenCreationService; + @Mock + private JWKResolver jwkResolver; private final List validationStrategyList = new ArrayList<>(); @@ -142,7 +139,7 @@ private ZosmfService getZosmfServiceSpy() { applicationContext, authenticationService, null, - null); + jwkResolver); ZosmfService zosmfService = spy(zosmfServiceObj); doReturn(ZOSMF_ID).when(zosmfService).getZosmfServiceId(); doReturn("http://zosmf:1433").when(zosmfService).getURI(ZOSMF_ID); @@ -717,16 +714,13 @@ class WhenReadTokenFromCookie { """; @Test - void thenSuccess() throws JSONException, IOException { + void thenSuccess() throws JSONException, IOException, JoseException { String zosmfJwtUrl = "/jwt/ibm/api/zOSMFBuilder/jwk"; when(authConfigurationProperties.getZosmf().getJwtEndpoint()).thenReturn(zosmfJwtUrl); ZosmfService zosmfService = getZosmfServiceSpy(); - DefaultResourceRetriever resourceRetriever = mock(DefaultResourceRetriever.class); - ReflectionTestUtils.setField(zosmfService, "resourceRetriever", resourceRetriever); - - when(resourceRetriever.retrieveResource(any())).thenReturn(new Resource(ZOSMF_PUBLIC_KEY_JSON, null)); - JSONAssert.assertEquals(ZOSMF_PUBLIC_KEY_JSON, new JSONObject(zosmfService.getPublicKeys().toString()), true); + when(jwkResolver.resolve(any())).thenReturn(new JsonWebKeySet(ZOSMF_PUBLIC_KEY_JSON)); + JSONAssert.assertEquals(ZOSMF_PUBLIC_KEY_JSON, new JSONObject(zosmfService.getPublicKeys().toJson()), true); } @Test @@ -746,9 +740,10 @@ void thenReturnNull() { class WhenGetsPublicKeys { @Test - void givenExceptionInTheResponse_thenPublicKeysAreEmpty() { + void givenExceptionInTheResponse_thenPublicKeysAreEmpty() throws JoseException, IOException { ZosmfService zosmfService = getZosmfServiceSpy(); - assertTrue(zosmfService.getPublicKeys().getKeys().isEmpty()); + when(jwkResolver.resolve(any())).thenThrow(IOException.class); + assertTrue(zosmfService.getPublicKeys().getJsonWebKeys().isEmpty()); } }