From 06a1d45b737b66766b7491a87496467f21420830 Mon Sep 17 00:00:00 2001 From: Pablo Carle Date: Fri, 10 Oct 2025 12:11:45 +0200 Subject: [PATCH 01/29] add josejwt Signed-off-by: Pablo Carle --- zaas-service/build.gradle | 3 + .../service/AuthenticationService.java | 99 +++++++++++++++++-- 2 files changed, 92 insertions(+), 10 deletions(-) diff --git a/zaas-service/build.gradle b/zaas-service/build.gradle index de809c8f40..d5970f9db6 100644 --- a/zaas-service/build.gradle +++ b/zaas-service/build.gradle @@ -101,6 +101,9 @@ dependencies { implementation libs.jjwt.impl implementation libs.jjwt.jackson + // implementation("com.ibm.websphere.appserver.api:com.ibm.websphere.appserver.api.jwt:1.1.106") + // implementation("io.openliberty.api:io.openliberty.jwt:1.1.106") + implementation("org.bitbucket.b_c:jose4j:0.9.6") compileOnly libs.lombok annotationProcessor libs.lombok 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..14551e7fe4 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 @@ -25,6 +25,14 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang.StringUtils; +import org.jose4j.jwa.AlgorithmConstraints; +import org.jose4j.jwa.AlgorithmConstraints.ConstraintType; +import org.jose4j.jwe.ContentEncryptionAlgorithmIdentifiers; +import org.jose4j.jwe.JsonWebEncryption; +import org.jose4j.jwe.KeyManagementAlgorithmIdentifiers; +import org.jose4j.keys.AesKey; +import org.jose4j.lang.ByteUtil; +import org.jose4j.lang.JoseException; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.cache.CacheManager; import org.springframework.cache.annotation.CacheEvict; @@ -42,14 +50,26 @@ 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 java.security.Key; +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.zaas.security.service.JwtUtils.getJwtClaims; import static org.zowe.apiml.zaas.security.service.JwtUtils.handleJwtParserException; @@ -121,14 +141,73 @@ 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 { + // SignatureAlgorithm alg = Jwts.SIG.RS256; + + // RsaPrivateJwk privateJwk = Jwks.builder().key((RSAPrivateKey)jwtSecurityInitializer.getJwtSecret()).publicKey(((RSAPublicKey)jwtSecurityInitializer.getJwtPublicKey())).build(); + + // // String jws = Jwts.builder().subject("Alice") + // // .signWith(pair.getPrivate(), alg) // <-- Bob's RSA private key + // // .compact(); + + // return Jwts.builder() + // .subject(username) + // .issuedAt(new Date(issuedAt)) + // .expiration(new Date(expiration)) + // .issuer(issuer) + // .id(UUID.randomUUID().toString()) + // .claims(claims) + // .signWith(privateJwk.toKey(), alg).compact(); + // //.signWith(jwtSecurityInitializer.getJwtSecret(), jwtSecurityInitializer.getSignatureAlgorithm()).compact(); + // } catch (Exception e) { + // log.error("Exception in io.jsonwebtoken implementation: {}", e.getMessage(), e); + // } + + + // try { + // return JwtBuilder.create() + // .subject(username) + // .expirationTime(new Date(expiration).getTime()) + // .issuer(issuer) + // .claim(claims) + // .signWith(jwtSecurityInitializer.getSignatureAlgorithm().getId(), jwtSecurityInitializer.getJwtSecret()) + // .buildJwt().compact(); + // } catch (JwtException e) { + // log.error("Jwt exception: {}", e.getMessage(), e); + // return null; + // } catch (InvalidBuilderException e) { + // log.error("InvalidBuilder exception: {}", e.getMessage(), e); + // return null; + // } catch (InvalidClaimException e) { + // log.error("InvalidClaim exception: {}", e.getMessage(), e); + // return null; + // } catch (KeyException e) { + // log.error("Key exception: {}", e.getMessage(), e); + // return null; + // } catch (Exception e ) { + // log.error("Exception: {}", e.getMessage(), e); + // return null; + // } + try { + JsonWebEncryption jwe = new JsonWebEncryption(); + jwe.setPayload("Hello World!"); + jwe.setAlgorithmHeaderValue(KeyManagementAlgorithmIdentifiers.RSA_OAEP_256); // RS256 + jwe.setEncryptionMethodHeaderParameter(ContentEncryptionAlgorithmIdentifiers.AES_128_CBC_HMAC_SHA_256); // RS256 + jwe.setKey(jwtSecurityInitializer.getJwtSecret()); + String serializedJwe = jwe.getCompactSerialization(); + System.out.println("Serialized Encrypted JWE: " + serializedJwe); + jwe = new JsonWebEncryption(); + jwe.setAlgorithmConstraints(new AlgorithmConstraints(ConstraintType.PERMIT, + KeyManagementAlgorithmIdentifiers.A128KW)); // RS256 + jwe.setContentEncryptionAlgorithmConstraints(new AlgorithmConstraints(ConstraintType.PERMIT, + ContentEncryptionAlgorithmIdentifiers.AES_128_CBC_HMAC_SHA_256)); + jwe.setKey(jwtSecurityInitializer.getJwtSecret()); + jwe.setCompactSerialization(serializedJwe); + System.out.println("Payload: " + jwe.getPayload()); // payload is the jwt + return jwe.getPayload(); + } catch (JoseException e) { + log.error("JoseException: {}", e.getMessage(), e); + } } @SuppressWarnings("java:S5659") From 2ced5c9352d4b9a3f33ef65c594a27a8e771ff93 Mon Sep 17 00:00:00 2001 From: Pablo Carle Date: Fri, 10 Oct 2025 12:18:18 +0200 Subject: [PATCH 02/29] update poc Signed-off-by: Pablo Carle --- .../service/AuthenticationService.java | 49 ++++++++++--------- 1 file changed, 25 insertions(+), 24 deletions(-) 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 14551e7fe4..b47689e2c8 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 @@ -25,13 +25,9 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang.StringUtils; -import org.jose4j.jwa.AlgorithmConstraints; -import org.jose4j.jwa.AlgorithmConstraints.ConstraintType; -import org.jose4j.jwe.ContentEncryptionAlgorithmIdentifiers; -import org.jose4j.jwe.JsonWebEncryption; -import org.jose4j.jwe.KeyManagementAlgorithmIdentifiers; -import org.jose4j.keys.AesKey; -import org.jose4j.lang.ByteUtil; +import org.jose4j.jws.AlgorithmIdentifiers; +import org.jose4j.jws.JsonWebSignature; +import org.jose4j.jwt.JwtClaims; import org.jose4j.lang.JoseException; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.cache.CacheManager; @@ -61,7 +57,6 @@ import org.zowe.apiml.zaas.security.service.schema.source.AuthSource; import org.zowe.apiml.zaas.security.service.zosmf.ZosmfService; -import java.security.Key; import java.util.Arrays; import java.util.Collection; import java.util.Collections; @@ -189,24 +184,30 @@ private String createJWT(String username, String issuer, Map cla // return null; // } try { - JsonWebEncryption jwe = new JsonWebEncryption(); - jwe.setPayload("Hello World!"); - jwe.setAlgorithmHeaderValue(KeyManagementAlgorithmIdentifiers.RSA_OAEP_256); // RS256 - jwe.setEncryptionMethodHeaderParameter(ContentEncryptionAlgorithmIdentifiers.AES_128_CBC_HMAC_SHA_256); // RS256 - jwe.setKey(jwtSecurityInitializer.getJwtSecret()); - String serializedJwe = jwe.getCompactSerialization(); - System.out.println("Serialized Encrypted JWE: " + serializedJwe); - jwe = new JsonWebEncryption(); - jwe.setAlgorithmConstraints(new AlgorithmConstraints(ConstraintType.PERMIT, - KeyManagementAlgorithmIdentifiers.A128KW)); // RS256 - jwe.setContentEncryptionAlgorithmConstraints(new AlgorithmConstraints(ConstraintType.PERMIT, - ContentEncryptionAlgorithmIdentifiers.AES_128_CBC_HMAC_SHA_256)); - jwe.setKey(jwtSecurityInitializer.getJwtSecret()); - jwe.setCompactSerialization(serializedJwe); - System.out.println("Payload: " + jwe.getPayload()); // payload is the jwt - return jwe.getPayload(); + // Create the Claims, which will be the content of the JWT + JwtClaims newClaims = new JwtClaims(); + newClaims.setIssuer("APIML"); // who creates the token and signs it + newClaims.setAudience("Audience"); // to whom the token is intended to be sent + newClaims.setExpirationTimeMinutesInTheFuture(10); // time when the token will expire (10 minutes from now) + newClaims.setGeneratedJwtId(); // a unique identifier for the token + newClaims.setIssuedAtToNow(); // when the token was issued/created (now) + newClaims.setNotBeforeMinutesInThePast(2); // time before which the token is not yet valid (2 minutes ago) + newClaims.setSubject("PROD001"); // the subject/principal is whom the token is about + List groups = Arrays.asList("IZUADM"); + newClaims.setStringListClaim("groups", groups); // multi-valued claims work too and will end up as a JSON array + + // A JWT is a JWS and/or a JWE with JSON claims as the payload. + // In this example it is a JWS so we create a JsonWebSignature object. + JsonWebSignature jws = new JsonWebSignature(); + jws.setPayload(newClaims.toJson()); + jws.setKey(jwtSecurityInitializer.getJwtSecret()); + jws.setAlgorithmHeaderValue(AlgorithmIdentifiers.RSA_USING_SHA256); + String jwt = jws.getCompactSerialization(); + log.error("created jwt: {}", jwt); + return jwt; } catch (JoseException e) { log.error("JoseException: {}", e.getMessage(), e); + return null; } } From 0292adf6e560cbf45b979f633cc96004239e19c0 Mon Sep 17 00:00:00 2001 From: Elena Kubantseva Date: Mon, 13 Oct 2025 08:35:00 +0200 Subject: [PATCH 03/29] update claims and jws setup Signed-off-by: Elena Kubantseva --- .../security/service/AuthenticationService.java | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) 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 b47689e2c8..6015e9862a 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 @@ -28,6 +28,7 @@ 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.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.cache.CacheManager; @@ -186,22 +187,22 @@ private String createJWT(String username, String issuer, Map cla try { // Create the Claims, which will be the content of the JWT JwtClaims newClaims = new JwtClaims(); - newClaims.setIssuer("APIML"); // who creates the token and signs it - newClaims.setAudience("Audience"); // to whom the token is intended to be sent - newClaims.setExpirationTimeMinutesInTheFuture(10); // time when the token will expire (10 minutes from now) + + newClaims.setIssuer(issuer); // who creates the token and signs it + newClaims.setExpirationTime(NumericDate.fromMilliseconds(expiration)); // time when the token will expire (10 minutes from now) newClaims.setGeneratedJwtId(); // a unique identifier for the token - newClaims.setIssuedAtToNow(); // when the token was issued/created (now) - newClaims.setNotBeforeMinutesInThePast(2); // time before which the token is not yet valid (2 minutes ago) - newClaims.setSubject("PROD001"); // the subject/principal is whom the token is about - List groups = Arrays.asList("IZUADM"); - newClaims.setStringListClaim("groups", groups); // multi-valued claims work too and will end up as a JSON array + newClaims.setIssuedAt(NumericDate.fromMilliseconds(issuedAt)); // when the token was issued/created (now) + newClaims.setSubject(username); // the subject/principal is whom the token is about + newClaims.getClaimsMap().putAll(claims); // A JWT is a JWS and/or a JWE with JSON claims as the payload. // In this example it is a JWS so we create a JsonWebSignature object. JsonWebSignature jws = new JsonWebSignature(); jws.setPayload(newClaims.toJson()); jws.setKey(jwtSecurityInitializer.getJwtSecret()); + jws.setHeader("typ", "JWT"); jws.setAlgorithmHeaderValue(AlgorithmIdentifiers.RSA_USING_SHA256); + jws.setDoKeyValidation(false); String jwt = jws.getCompactSerialization(); log.error("created jwt: {}", jwt); return jwt; From c07ce9994f24d51e2b9194bc2a696388eadf32b1 Mon Sep 17 00:00:00 2001 From: Pablo Carle Date: Wed, 15 Oct 2025 18:12:04 +0200 Subject: [PATCH 04/29] wip, compiles using nimbus for parsing and jose4j for signing Signed-off-by: Pablo Carle --- apiml-package/src/main/resources/bin/start.sh | 24 ++-- apiml-security-common/build.gradle | 2 + .../apiml/security/common/util}/JwtUtils.java | 72 +++++++----- apiml/build.gradle | 1 + .../ReactivePublicJWKController.java | 47 ++++---- gateway-service/build.gradle | 7 +- .../CustomLoadBalancerConfiguration.java | 5 +- .../DeterministicLoadBalancer.java | 47 ++++---- ...terministicRoutingListSupplierBuilder.java | 5 +- gradle/versions.gradle | 2 + zaas-service/build.gradle | 10 +- .../zaas/controllers/AuthController.java | 63 +++++----- .../service/AuthenticationService.java | 102 +++++++++++------ .../zaas/security/service/JwtSecurity.java | 48 +++++--- .../ModulithAuthenticationService.java | 4 +- .../schema/source/OIDCAuthSourceService.java | 2 +- .../security/service/token/OIDCConfig.java | 6 +- .../service/token/OIDCTokenProvider.java | 108 ++++++++---------- .../security/service/zosmf/ZosmfService.java | 35 +++--- .../org/zowe/apiml/zaas/utils/JWTUtils.java | 1 + 20 files changed, 320 insertions(+), 271 deletions(-) rename {zaas-service/src/main/java/org/zowe/apiml/zaas/security/service => apiml-security-common/src/main/java/org/zowe/apiml/security/common/util}/JwtUtils.java (70%) diff --git a/apiml-package/src/main/resources/bin/start.sh b/apiml-package/src/main/resources/bin/start.sh index e1a7959e6c..8d01710ba0 100755 --- a/apiml-package/src/main/resources/bin/start.sh +++ b/apiml-package/src/main/resources/bin/start.sh @@ -124,12 +124,16 @@ if [ -z "${LIBRARY_PATH}" ]; then LIBRARY_PATH="../common-java-lib/bin/" fi -if [ "${ZWE_configs_debug:-${ZWE_components_gateway_debug:-${ZWE_components_discovery_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 - ZWE_configs_spring_profiles_active="${ZWE_configs_spring_profiles_active:-${ZWE_components_gateway_spring_profiles_active:-${ZWE_components_discovery_spring_profiles_active}}}," +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}debug" + ZWE_configs_spring_profiles_active="${ZWE_configs_spring_profiles_active}${new_profile}" +} + +if [ "${ZWE_configs_debug:-${ZWE_components_gateway_debug:-${ZWE_components_discovery_debug:-false}}}" = "true" ]; then + add_profile "debug" fi if [ "${ZWE_configs_apiml_security_auth_uniqueCookie:-${ZWE_components_gateway_apiml_security_auth_uniqueCookie:-false}}" = "true" ]; then @@ -180,14 +184,6 @@ if [ -n "${ZWE_configs_logging_config}" ]; then LOGBACK="-Dlogging.config=${ZWE_configs_logging_config}" 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}" -} - ATTLS_SERVER_ENABLED="false" ATTLS_CLIENT_ENABLED="false" @@ -401,7 +397,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..7f7fee1c58 100644 --- a/apiml-security-common/build.gradle +++ b/apiml-security-common/build.gradle @@ -12,6 +12,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"))) 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..52c612e9d3 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) { 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,26 @@ 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 (claim instanceof Map val) { + claim = val.get(key); + } } } - return extractValueAsList(val.get(key)); + return extractValueAsList(((Map) claim).get(key)); } @SuppressWarnings("unchecked") @@ -157,6 +172,7 @@ private List extractValueAsList(Object rawValue) { } else { throw new IllegalArgumentException("Field value is neither String nor List of Strings"); } + } } 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..3a514ca668 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,9 @@ 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.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; @@ -43,8 +43,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 +79,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 +119,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 +150,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 +162,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(); + PublicKey key = (PublicKey) publicKeys.get(0) + .getKey(); return new ResponseEntity<>(getPublicKeyAsPem(key), HttpStatus.OK); - } catch (IOException | JOSEException ex) { + } catch (IOException 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 +181,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 +196,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/gateway-service/build.gradle b/gateway-service/build.gradle index 5e2ccbcdb7..ca6a120990 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/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-service/build.gradle b/zaas-service/build.gradle index d5970f9db6..8de577c673 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,11 +97,8 @@ dependencies { implementation libs.caffeine - implementation libs.jjwt.impl - implementation libs.jjwt.jackson - // implementation("com.ibm.websphere.appserver.api:com.ibm.websphere.appserver.api.jwt:1.1.106") - // implementation("io.openliberty.api:io.openliberty.jwt:1.1.106") - implementation("org.bitbucket.b_c:jose4j:0.9.6") + implementation libs.nimbus.jose.jwt // Parsing + implementation libs.jose4j.jwt // Signing, with support for JCA with ICSF compileOnly libs.lombok annotationProcessor libs.lombok @@ -117,6 +112,7 @@ dependencies { testImplementation libs.rest.assured testImplementation libs.rest.assured.json.path testImplementation libs.rest.assured.xml.path + testImplementation libs.jjwt testCompileOnly libs.lombok testAnnotationProcessor libs.lombok 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..b136e7ee33 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,22 @@ 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.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 +59,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 +352,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 +392,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 +425,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 +435,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(); + PublicKey key = (PublicKey) publicKeys.get(0) + .getKey(); return new ResponseEntity<>(getPublicKeyAsPem(key), HttpStatus.OK); - } catch (IOException | JOSEException ex) { + } catch (IOException 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 +459,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 6015e9862a..ef7bbe477d 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.JWSHeader; +import com.nimbusds.jose.crypto.factories.DefaultJWSVerifierFactory; +import com.nimbusds.jwt.JWTClaimsSet; +import com.nimbusds.jwt.JWTParser; +import com.nimbusds.jwt.SignedJWT; +import com.nimbusds.jwt.proc.BadJWTException; import jakarta.annotation.PostConstruct; import jakarta.servlet.http.Cookie; import jakarta.servlet.http.HttpServletRequest; @@ -58,6 +60,8 @@ import org.zowe.apiml.zaas.security.service.schema.source.AuthSource; import org.zowe.apiml.zaas.security.service.zosmf.ZosmfService; +import java.text.ParseException; +import java.time.Clock; import java.util.Arrays; import java.util.Collection; import java.util.Collections; @@ -67,8 +71,8 @@ import java.util.Optional; import java.util.Set; -import static org.zowe.apiml.zaas.security.service.JwtUtils.getJwtClaims; -import static org.zowe.apiml.zaas.security.service.JwtUtils.handleJwtParserException; +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; @@ -98,6 +102,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; @@ -214,14 +219,10 @@ private String createJWT(String username, String issuer, Map cla @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); } @@ -344,15 +345,28 @@ 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 header = JWSHeader.parse(signedJwt.getSignature()); + var verifier = new DefaultJWSVerifierFactory().createJWSVerifier(header, jwtSecurityInitializer.getJwtPublicKey()); + var verified = signedJwt.verify(verifier); + if (verified) { + var claims = parsedJwt.getJWTClaimsSet(); + if (claims.getExpirationTime().toInstant().isBefore(clock.instant())) { + log.debug("OIDC Token is expired"); + return null; + } + 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); } } @@ -460,25 +474,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); + } } /** @@ -488,7 +506,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); } @@ -501,7 +519,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); + } } /** @@ -512,9 +534,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/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..2162f96705 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.JWSHeader; +import com.nimbusds.jose.crypto.factories.DefaultJWSVerifierFactory; +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,25 @@ 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.HttpsJwks; +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.text.ParseException; +import java.time.Clock; import java.util.List; import java.util.Map; -import java.util.Optional; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; @@ -53,9 +54,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,16 +62,15 @@ 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 CloseableHttpClient secureHttpClientWithKeystore; @Getter - private final Map publicKeys = new ConcurrentHashMap<>(); + private final Map publicKeys = new ConcurrentHashMap<>(); @Getter - private JWKSet jwkSet; + private JsonWebKeySet jwkSet; @PostConstruct @@ -85,7 +82,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 +91,36 @@ 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 httpsJwks = new HttpsJwks(url); + + var keySet = new JsonWebKeySet(httpsJwks.getJsonWebKeys()); + 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 -> 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,46 +142,35 @@ 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 (jwkSet == null || jwkSet.getJsonWebKeys().isEmpty()) { fetchJWKSet(); } if (StringUtils.isBlank(token)) { - throw new JwtException("Empty string provided instead of a token."); + throw new BadJOSEException("Empty string provided instead of a token."); } log.debug("Validating the token with JWK"); - return Jwts.parser() - .clock(clock) - .keyLocator(keyLocator) - .build() - .parseSignedClaims(token) - .getPayload(); - } - - class LocatorAdapterKid extends LocatorAdapter { - - @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 kid = header.getKeyId(); - if (kid == null) { - throw new UnsupportedKeyException("Token does not provide kid. It uses an unsupported type of signature."); + var jwt = JWTParser.parse(token); + if (jwt instanceof SignedJWT signedJwt) { + var header = JWSHeader.parse(signedJwt.getSignature()); + var verifier = new DefaultJWSVerifierFactory().createJWSVerifier(header, publicKeys.get(header.getKeyID()).getKey()); + var verified = signedJwt.verify(verifier); + if (verified) { + var claims = jwt.getJWTClaimsSet(); + if (claims.getExpirationTime().toInstant().isBefore(clock.instant())) { + log.debug("OIDC Token is expired"); + return null; + } + return claims; } - 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 { + log.debug("OIDC Token is not signed"); } + return null; } 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..e5ed331d8d 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,9 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang.StringUtils; +import org.jose4j.jwk.HttpsJwks; +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; @@ -61,7 +61,7 @@ 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; @@ -128,7 +128,6 @@ public static class ZosmfInfo { private ZosmfService meAsProxy; private TokenCreationService tokenCreationService; - private final DefaultResourceRetriever resourceRetriever; public ZosmfService( final AuthConfigurationProperties authConfigurationProperties, @@ -136,8 +135,7 @@ public ZosmfService( final ObjectMapper securityObjectMapper, final ApplicationContext applicationContext, final AuthenticationService authenticationService, - List tokenValidationStrategy, - DefaultResourceRetriever resourceRetriever + List tokenValidationStrategy ) { super( applicationContext, @@ -147,7 +145,6 @@ public ZosmfService( ); this.tokenValidationStrategy = tokenValidationStrategy; this.authenticationService = authenticationService; - this.resourceRetriever = resourceRetriever; } private final AuthenticationService authenticationService; @@ -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,15 @@ 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()); + var httpsJwks = new HttpsJwks(jwkZosmfUrl); 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 new JsonWebKeySet(httpsJwks.getJsonWebKeys()); + } 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/zaas/utils/JWTUtils.java b/zaas-service/src/test/java/org/zowe/apiml/zaas/utils/JWTUtils.java index e0114f3a55..4021098b8b 100644 --- a/zaas-service/src/test/java/org/zowe/apiml/zaas/utils/JWTUtils.java +++ b/zaas-service/src/test/java/org/zowe/apiml/zaas/utils/JWTUtils.java @@ -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) From 3461a55ef1e217a935b3155802f7c52437f1d7ca Mon Sep 17 00:00:00 2001 From: Pablo Carle Date: Thu, 16 Oct 2025 08:14:12 +0200 Subject: [PATCH 05/29] test for nimbus signing Signed-off-by: Pablo Carle --- .../service/AuthenticationService.java | 88 ++++++++++++++----- 1 file changed, 65 insertions(+), 23 deletions(-) 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 ef7bbe477d..44053dc751 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 @@ -14,8 +14,19 @@ import com.netflix.discovery.EurekaClient; import com.netflix.discovery.shared.Application; import com.nimbusds.jose.JOSEException; +import com.nimbusds.jose.JWSAlgorithm; import com.nimbusds.jose.JWSHeader; +import com.nimbusds.jose.JWSSigner; +import com.nimbusds.jose.JWSVerifier; +import com.nimbusds.jose.crypto.RSASSASigner; +import com.nimbusds.jose.crypto.RSASSAVerifier; +import com.nimbusds.jose.crypto.factories.DefaultJWSSignerFactory; import com.nimbusds.jose.crypto.factories.DefaultJWSVerifierFactory; +import com.nimbusds.jose.crypto.impl.RSAKeyUtils; +import com.nimbusds.jose.jwk.JWK; +import com.nimbusds.jose.jwk.RSAKey; +import com.nimbusds.jose.jwk.gen.JWKGenerator; +import com.nimbusds.jose.produce.JWSSignerFactory; import com.nimbusds.jwt.JWTClaimsSet; import com.nimbusds.jwt.JWTParser; import com.nimbusds.jwt.SignedJWT; @@ -27,10 +38,12 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang.StringUtils; +import org.bouncycastle.crypto.generators.RSAKeyPairGenerator; import org.jose4j.jws.AlgorithmIdentifiers; import org.jose4j.jws.JsonWebSignature; import org.jose4j.jwt.JwtClaims; import org.jose4j.jwt.NumericDate; +import org.jose4j.keys.RsaKeyUtil; import org.jose4j.lang.JoseException; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.cache.CacheManager; @@ -60,11 +73,13 @@ import org.zowe.apiml.zaas.security.service.schema.source.AuthSource; import org.zowe.apiml.zaas.security.service.zosmf.ZosmfService; +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.Date; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -191,29 +206,56 @@ private String createJWT(String username, String issuer, Map cla // } try { // Create the Claims, which will be the content of the JWT - JwtClaims newClaims = new JwtClaims(); - - newClaims.setIssuer(issuer); // who creates the token and signs it - newClaims.setExpirationTime(NumericDate.fromMilliseconds(expiration)); // time when the token will expire (10 minutes from now) - newClaims.setGeneratedJwtId(); // a unique identifier for the token - newClaims.setIssuedAt(NumericDate.fromMilliseconds(issuedAt)); // when the token was issued/created (now) - newClaims.setSubject(username); // the subject/principal is whom the token is about - newClaims.getClaimsMap().putAll(claims); - - // A JWT is a JWS and/or a JWE with JSON claims as the payload. - // In this example it is a JWS so we create a JsonWebSignature object. - JsonWebSignature jws = new JsonWebSignature(); - jws.setPayload(newClaims.toJson()); - jws.setKey(jwtSecurityInitializer.getJwtSecret()); - jws.setHeader("typ", "JWT"); - jws.setAlgorithmHeaderValue(AlgorithmIdentifiers.RSA_USING_SHA256); - jws.setDoKeyValidation(false); - String jwt = jws.getCompactSerialization(); - log.error("created jwt: {}", jwt); - return jwt; - } catch (JoseException e) { - log.error("JoseException: {}", e.getMessage(), e); - return null; + + // This one works, + // JwtClaims newClaims = new JwtClaims(); + + // newClaims.setIssuer(issuer); // who creates the token and signs it + // newClaims.setExpirationTime(NumericDate.fromMilliseconds(expiration)); // time when the token will expire (10 minutes from now) + // newClaims.setGeneratedJwtId(); // a unique identifier for the token + // newClaims.setIssuedAt(NumericDate.fromMilliseconds(issuedAt)); // when the token was issued/created (now) + // newClaims.setSubject(username); // the subject/principal is whom the token is about + // newClaims.getClaimsMap().putAll(claims); + + // // A JWT is a JWS and/or a JWE with JSON claims as the payload. + // // In this example it is a JWS so we create a JsonWebSignature object. + // JsonWebSignature jws = new JsonWebSignature(); + // jws.setPayload(newClaims.toJson()); + // jws.setKey(jwtSecurityInitializer.getJwtSecret()); + // jws.setHeader("typ", "JWT"); + // jws.setAlgorithmHeaderValue(AlgorithmIdentifiers.RSA_USING_SHA256); + // jws.setDoKeyValidation(false); + // String jwt = jws.getCompactSerialization(); + // log.error("created jwt: {}", jwt); + // return jwt; + + + RSAKey rsaJWK = new RSAKey.Builder((RSAPublicKey)jwtSecurityInitializer.getJwtPublicKey()) + .privateKey(jwtSecurityInitializer.getJwtSecret()).build(); + JWSSigner signer = new RSASSASigner(rsaJWK); + JWTClaimsSet claimsSet = new JWTClaimsSet.Builder() + .subject("alice") + .issuer("https://c2id.com") + .expirationTime(new Date(new Date().getTime() + 60 * 1000)) + .build(); + SignedJWT signedJWT = new SignedJWT( + new JWSHeader.Builder(JWSAlgorithm.RS256).keyID(rsaJWK.getKeyID()).build(), + claimsSet); + + signedJWT.sign(signer); + + String s = signedJWT.serialize(); + log.error("created jwt: {}", s); + + signedJWT = SignedJWT.parse(s); + + JWSVerifier verifier = new RSASSAVerifier((RSAPublicKey) jwtSecurityInitializer.getJwtPublicKey()); + + log.error("token is valid for public key: {}", signedJWT.verify(verifier)); + + } catch (JOSEException | ParseException e) { + // TODO Auto-generated catch block + e.printStackTrace(); } } From c045085384857a584997256c42e2242b7092eba5 Mon Sep 17 00:00:00 2001 From: Pablo Carle Date: Thu, 16 Oct 2025 16:21:07 +0200 Subject: [PATCH 06/29] fix test compile issues Signed-off-by: Pablo Carle --- .../ReactivePublicJWKControllerTest.java | 138 +++++++++++++----- .../DeterministicLoadBalancerTest.java | 9 +- .../service/AuthenticationService.java | 132 +++-------------- .../zaas/controllers/AuthControllerTest.java | 86 ++++++----- .../ZosmfAuthenticationProviderTest.java | 3 +- .../query/SuccessfulQueryHandlerTest.java | 19 ++- .../service/AuthenticationServiceTest.java | 46 ++++-- .../security/service/JwtSecurityTest.java | 27 ++-- .../zaas/security/service/JwtUtilsTest.java | 1 + .../service/token/OIDCTokenProviderTest.java | 8 +- .../service/zosmf/ZosmfServiceTest.java | 10 +- 11 files changed, 242 insertions(+), 237 deletions(-) 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..435f1a8eb6 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,15 @@ package org.zowe.apiml.controller; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; import com.nimbusds.jose.JOSEException; import com.nimbusds.jose.jwk.JWK; import com.nimbusds.jose.jwk.JWKSet; import com.nimbusds.jose.jwk.RSAKey; +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; @@ -36,12 +41,14 @@ 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.junit.jupiter.api.Assertions.fail; import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.lenient; import static org.mockito.Mockito.mock; @@ -61,49 +68,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((RSAPublicKey) generateKeyPair().getPublic()); + zosmfJwk.setKeyId("zosmfKey"); + var zosmfKeySet = new JsonWebKeySet(zosmfJwk); + var apimlJwk = JsonWebKey.Factory.newJwk((RSAPublicKey) generateKeyPair().getPublic()); + apimlJwk.setKeyId("apimlKey"); + var oidcJwk = JsonWebKey.Factory.newJwk((RSAPublicKey) 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((RSAPublicKey) 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 +146,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((RSAPublicKey) 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 +173,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((RSAPublicKey) 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 +202,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 +222,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((RSAPublicKey) keyPair.getPublic()); + jwk.setKeyId("signingKey"); + var keySet = new JsonWebKeySet(jwk); when(jwtSecurity.actualJwtProducer()).thenReturn(JwtSecurity.JwtProducer.APIML); when(jwtSecurity.getPublicKeyInSet()).thenReturn(keySet); @@ -202,11 +257,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((RSAPublicKey) kp1.getPublic()); + jwk1.setKeyId("key1"); + var jwk2 = JsonWebKey.Factory.newJwk((RSAPublicKey) 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 +278,6 @@ void givenMultipleKeys_thenReturnErrorWithCorrectMessage() throws Exception { lenient().when(mockApiMessage.mapToApiMessage()).thenReturn(expectedApiMessage); - Mono> result = controller.getPublicKeyUsedForSigning(); StepVerifier.create(result) @@ -233,13 +291,15 @@ 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(); + var keyPair = KeyPairGenerator.getInstance("RSA").generateKeyPair(); + var realRsaKey = new RSAKey.Builder((RSAPublicKey) keyPair.getPublic()).build(); - RSAKey spyRsaKey = spy(realRsaKey); + var spyRsaKey = spy(realRsaKey); doThrow(new JOSEException("Test JOSE Exception")).when(spyRsaKey).toPublicKey(); - JWKSet keySet = new JWKSet(List.of(spyRsaKey)); + var jwk = JsonWebKey.Factory.newJwk(keyPair.getPublic()); + + var keySet = new JsonWebKeySet(List.of(jwk)); when(jwtSecurity.actualJwtProducer()).thenReturn(JwtSecurity.JwtProducer.APIML); when(jwtSecurity.getPublicKeyInSet()).thenReturn(keySet); @@ -249,7 +309,7 @@ void getPublicKeyUsedForSigning_joseException() throws Exception { 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/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/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 44053dc751..51f6ecbdb5 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 @@ -14,19 +14,8 @@ import com.netflix.discovery.EurekaClient; import com.netflix.discovery.shared.Application; import com.nimbusds.jose.JOSEException; -import com.nimbusds.jose.JWSAlgorithm; import com.nimbusds.jose.JWSHeader; -import com.nimbusds.jose.JWSSigner; -import com.nimbusds.jose.JWSVerifier; -import com.nimbusds.jose.crypto.RSASSASigner; -import com.nimbusds.jose.crypto.RSASSAVerifier; -import com.nimbusds.jose.crypto.factories.DefaultJWSSignerFactory; import com.nimbusds.jose.crypto.factories.DefaultJWSVerifierFactory; -import com.nimbusds.jose.crypto.impl.RSAKeyUtils; -import com.nimbusds.jose.jwk.JWK; -import com.nimbusds.jose.jwk.RSAKey; -import com.nimbusds.jose.jwk.gen.JWKGenerator; -import com.nimbusds.jose.produce.JWSSignerFactory; import com.nimbusds.jwt.JWTClaimsSet; import com.nimbusds.jwt.JWTParser; import com.nimbusds.jwt.SignedJWT; @@ -38,13 +27,12 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang.StringUtils; -import org.bouncycastle.crypto.generators.RSAKeyPairGenerator; import org.jose4j.jws.AlgorithmIdentifiers; import org.jose4j.jws.JsonWebSignature; import org.jose4j.jwt.JwtClaims; import org.jose4j.jwt.NumericDate; -import org.jose4j.keys.RsaKeyUtil; 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; @@ -73,13 +61,11 @@ import org.zowe.apiml.zaas.security.service.schema.source.AuthSource; import org.zowe.apiml.zaas.security.service.zosmf.ZosmfService; -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.Date; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -157,105 +143,25 @@ public String createLongLivedJwtToken(@NonNull String username, int daysToLive, } private String createJWT(String username, String issuer, Map claims, long issuedAt, long expiration) { - // try { - // SignatureAlgorithm alg = Jwts.SIG.RS256; - - // RsaPrivateJwk privateJwk = Jwks.builder().key((RSAPrivateKey)jwtSecurityInitializer.getJwtSecret()).publicKey(((RSAPublicKey)jwtSecurityInitializer.getJwtPublicKey())).build(); - - // // String jws = Jwts.builder().subject("Alice") - // // .signWith(pair.getPrivate(), alg) // <-- Bob's RSA private key - // // .compact(); - - // return Jwts.builder() - // .subject(username) - // .issuedAt(new Date(issuedAt)) - // .expiration(new Date(expiration)) - // .issuer(issuer) - // .id(UUID.randomUUID().toString()) - // .claims(claims) - // .signWith(privateJwk.toKey(), alg).compact(); - // //.signWith(jwtSecurityInitializer.getJwtSecret(), jwtSecurityInitializer.getSignatureAlgorithm()).compact(); - // } catch (Exception e) { - // log.error("Exception in io.jsonwebtoken implementation: {}", e.getMessage(), e); - // } - - - // try { - // return JwtBuilder.create() - // .subject(username) - // .expirationTime(new Date(expiration).getTime()) - // .issuer(issuer) - // .claim(claims) - // .signWith(jwtSecurityInitializer.getSignatureAlgorithm().getId(), jwtSecurityInitializer.getJwtSecret()) - // .buildJwt().compact(); - // } catch (JwtException e) { - // log.error("Jwt exception: {}", e.getMessage(), e); - // return null; - // } catch (InvalidBuilderException e) { - // log.error("InvalidBuilder exception: {}", e.getMessage(), e); - // return null; - // } catch (InvalidClaimException e) { - // log.error("InvalidClaim exception: {}", e.getMessage(), e); - // return null; - // } catch (KeyException e) { - // log.error("Key exception: {}", e.getMessage(), e); - // return null; - // } catch (Exception e ) { - // log.error("Exception: {}", e.getMessage(), e); - // return null; - // } try { - // Create the Claims, which will be the content of the JWT - - // This one works, - // JwtClaims newClaims = new JwtClaims(); - - // newClaims.setIssuer(issuer); // who creates the token and signs it - // newClaims.setExpirationTime(NumericDate.fromMilliseconds(expiration)); // time when the token will expire (10 minutes from now) - // newClaims.setGeneratedJwtId(); // a unique identifier for the token - // newClaims.setIssuedAt(NumericDate.fromMilliseconds(issuedAt)); // when the token was issued/created (now) - // newClaims.setSubject(username); // the subject/principal is whom the token is about - // newClaims.getClaimsMap().putAll(claims); - - // // A JWT is a JWS and/or a JWE with JSON claims as the payload. - // // In this example it is a JWS so we create a JsonWebSignature object. - // JsonWebSignature jws = new JsonWebSignature(); - // jws.setPayload(newClaims.toJson()); - // jws.setKey(jwtSecurityInitializer.getJwtSecret()); - // jws.setHeader("typ", "JWT"); - // jws.setAlgorithmHeaderValue(AlgorithmIdentifiers.RSA_USING_SHA256); - // jws.setDoKeyValidation(false); - // String jwt = jws.getCompactSerialization(); - // log.error("created jwt: {}", jwt); - // return jwt; - - - RSAKey rsaJWK = new RSAKey.Builder((RSAPublicKey)jwtSecurityInitializer.getJwtPublicKey()) - .privateKey(jwtSecurityInitializer.getJwtSecret()).build(); - JWSSigner signer = new RSASSASigner(rsaJWK); - JWTClaimsSet claimsSet = new JWTClaimsSet.Builder() - .subject("alice") - .issuer("https://c2id.com") - .expirationTime(new Date(new Date().getTime() + 60 * 1000)) - .build(); - SignedJWT signedJWT = new SignedJWT( - new JWSHeader.Builder(JWSAlgorithm.RS256).keyID(rsaJWK.getKeyID()).build(), - claimsSet); - - signedJWT.sign(signer); - - String s = signedJWT.serialize(); - log.error("created jwt: {}", s); - - signedJWT = SignedJWT.parse(s); - - JWSVerifier verifier = new RSASSAVerifier((RSAPublicKey) jwtSecurityInitializer.getJwtPublicKey()); - - log.error("token is valid for public key: {}", signedJWT.verify(verifier)); - - } catch (JOSEException | ParseException e) { - // TODO Auto-generated catch block - e.printStackTrace(); + 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); + newClaims.getClaimsMap().putAll(claims); + + 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); } } 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..14821bf42e 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,10 @@ 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 +44,33 @@ import org.zowe.apiml.zaas.security.webfinger.WebFingerResponse; import java.io.IOException; +import java.math.BigInteger; +import java.security.interfaces.RSAPublicKey; import java.text.ParseException; 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 +98,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 ParseException, 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 +137,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 ParseException, JoseException { + return JsonWebKey.Factory.newJwk("{" + "\"e\":\"AQAB\"," + "\"n\":\"kWp2zRA23Z3vTL4uoe8kTFptxBVFunIoP4t_8TDYJrOb7D1iZNDXVeEsYKp6ppmrTZDAgd-cNOTKLd4M39WJc5FN0maTAVKJc7NxklDeKc4dMe1BGvTZNG4MpWBo-taKULlYUu0ltYJuLzOjIrTHfarucrGoRWqM0sl3z2-fv9k\",\n" + "\"kty\":\"RSA\",\n" + @@ -130,13 +147,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,7 +161,7 @@ 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())); @@ -154,7 +171,7 @@ void testGetAllPublicKeys() throws Exception { 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())); @@ -163,12 +180,12 @@ void givenAPIMLJWTProducer_whenGetAllPublicKeys_thenReturnsOnlyAPIMLKeys() throw @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())); @@ -180,7 +197,7 @@ 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())); @@ -199,7 +216,7 @@ void returnEmptyWhenUnknown() throws Exception { @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)) @@ -214,7 +231,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 +240,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 +269,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 +284,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/login/zosmf/ZosmfAuthenticationProviderTest.java b/zaas-service/src/test/java/org/zowe/apiml/zaas/security/login/zosmf/ZosmfAuthenticationProviderTest.java index 186a1e1764..3d0fc00804 100644 --- a/zaas-service/src/test/java/org/zowe/apiml/zaas/security/login/zosmf/ZosmfAuthenticationProviderTest.java +++ b/zaas-service/src/test/java/org/zowe/apiml/zaas/security/login/zosmf/ZosmfAuthenticationProviderTest.java @@ -107,8 +107,7 @@ private ZosmfService createZosmfService() { securityObjectMapper, applicationContext, authenticationService, - new ArrayList<>(), - null); + new ArrayList<>()); ReflectionTestUtils.setField(zosmfService, "meAsProxy", zosmfService); ReflectionTestUtils.setField(zosmfService, "discovery", new CompositeDiscoveryClient(Collections.singletonList(new EurekaDiscoveryClient(eurekaClient, clientConfig)))); ReflectionTestUtils.setField(zosmfService, "tokenCreationService", tokenCreationService); 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..8e4ed6a552 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,13 @@ 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.when; @ExtendWith(MockitoExtension.class) @@ -79,13 +82,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) { @@ -96,15 +102,14 @@ void setup() { new ObjectMapper(), applicationContext, authenticationService, - new ArrayList<>(), - null); + new ArrayList<>()); ReflectionTestUtils.setField(zosmfService, "meAsProxy", zosmfService); ReflectionTestUtils.setField(zosmfService, "discovery", discoveryClient); ReflectionTestUtils.setField(zosmfService, "tokenCreationService", tokenCreationService); AuthenticationService authService = new AuthenticationService( applicationContext, authConfigurationProperties, jwtSecurityInitializer, zosmfService, - eurekaClient, restTemplate, cacheManager, new CacheUtils() + eurekaClient, restTemplate, cacheManager, new CacheUtils(), clock ); when(jwtSecurityInitializer.getSignatureAlgorithm()).thenReturn(algorithm); when(jwtSecurityInitializer.getJwtSecret()).thenReturn(privateKey); 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..159eedf7b6 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,31 @@ 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.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.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 +93,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 +118,8 @@ public class AuthenticationServiceTest { //NOSONAR, needs to be public private CacheUtils cacheUtils; @Mock private CacheManager cacheManager; + @Mock + private Clock clock; static { @@ -109,13 +132,12 @@ 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"); @@ -386,7 +408,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 +517,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)) { 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/JwtUtilsTest.java b/zaas-service/src/test/java/org/zowe/apiml/zaas/security/service/JwtUtilsTest.java index 925916bdc8..ce0995bde8 100644 --- a/zaas-service/src/test/java/org/zowe/apiml/zaas/security/service/JwtUtilsTest.java +++ b/zaas-service/src/test/java/org/zowe/apiml/zaas/security/service/JwtUtilsTest.java @@ -22,6 +22,7 @@ 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.security.common.util.JwtUtils; import java.util.Arrays; import java.util.Collections; 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..58bc1dbde4 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 @@ -15,7 +15,6 @@ 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; @@ -37,6 +36,7 @@ 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; @@ -49,7 +49,7 @@ 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.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; @@ -98,7 +98,7 @@ static void init() throws Exception { @BeforeEach void setup() throws CachingServiceClientException, IOException { - oidcTokenProvider = new OIDCTokenProvider(new DefaultClock(), resourceRetriever, httpClient); + oidcTokenProvider = new OIDCTokenProvider(Clock.systemUTC(), httpClient); ReflectionTestUtils.setField(oidcTokenProvider, "jwkRefreshInterval", 1); oktaJwks = Resources.toString(Resources.getResource(OKTA_JWKS_RESOURCE), StandardCharsets.UTF_8); } @@ -186,7 +186,7 @@ class JwksUriLoad { @BeforeEach public void setUp() { - oidcTokenProvider = new OIDCTokenProvider(new DefaultClock(), resourceRetriever, httpClient); + oidcTokenProvider = new OIDCTokenProvider(Clock.systemUTC(), httpClient); ReflectionTestUtils.setField(oidcTokenProvider, "jwksUri", List.of("https://jwksurl")); ReflectionTestUtils.setField(oidcTokenProvider, "resourceRetriever", resourceRetriever); } 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..bd40d0792d 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 @@ -141,7 +141,6 @@ private ZosmfService getZosmfServiceSpy() { securityObjectMapper, applicationContext, authenticationService, - null, null); ZosmfService zosmfService = spy(zosmfServiceObj); doReturn(ZOSMF_ID).when(zosmfService).getZosmfServiceId(); @@ -156,8 +155,7 @@ private ZosmfService getZosmfServiceWithValidationStrategy(List Date: Thu, 16 Oct 2025 16:24:23 +0200 Subject: [PATCH 07/29] fix checkstyle Signed-off-by: Pablo Carle --- .../zowe/apiml/controller/ReactivePublicJWKControllerTest.java | 2 -- 1 file changed, 2 deletions(-) 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 435f1a8eb6..666f6f0f5f 100644 --- a/apiml/src/test/java/org/zowe/apiml/controller/ReactivePublicJWKControllerTest.java +++ b/apiml/src/test/java/org/zowe/apiml/controller/ReactivePublicJWKControllerTest.java @@ -14,8 +14,6 @@ import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; import com.nimbusds.jose.JOSEException; -import com.nimbusds.jose.jwk.JWK; -import com.nimbusds.jose.jwk.JWKSet; import com.nimbusds.jose.jwk.RSAKey; import org.jose4j.jwk.JsonWebKey; import org.jose4j.jwk.JsonWebKeySet; From cc61763fb0cdfe0d016eb52e293169817a64401a Mon Sep 17 00:00:00 2001 From: Pablo Carle Date: Thu, 16 Oct 2025 16:33:26 +0200 Subject: [PATCH 08/29] fix test classpath Signed-off-by: Pablo Carle --- zaas-service/build.gradle | 2 ++ 1 file changed, 2 insertions(+) diff --git a/zaas-service/build.gradle b/zaas-service/build.gradle index 8de577c673..ab7593e8b9 100644 --- a/zaas-service/build.gradle +++ b/zaas-service/build.gradle @@ -113,6 +113,8 @@ dependencies { 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 From c1a8a31ead6bf0bd831a3a76b6297c67949b0bde Mon Sep 17 00:00:00 2001 From: Pablo Carle Date: Thu, 16 Oct 2025 16:43:54 +0200 Subject: [PATCH 09/29] fix few tests Signed-off-by: Pablo Carle --- .../apiml/zaas/controllers/AuthControllerTest.java | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) 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 14821bf42e..1ea8de5d82 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 @@ -164,7 +164,7 @@ void testGetAllPublicKeys() throws Exception { 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 @@ -174,7 +174,7 @@ void givenAPIMLJWTProducer_whenGetAllPublicKeys_thenReturnsOnlyAPIMLKeys() throw 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 @@ -188,7 +188,7 @@ void givenOIDCJWKSet_whenGetAllPublicKeys_thenIncludeOIDCInResult() throws Excep 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 @@ -200,17 +200,17 @@ void useZoweJwt() throws Exception { 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 From ab310cd32665ab68993ffbb57f6e65991311404f Mon Sep 17 00:00:00 2001 From: Pablo Carle Date: Thu, 16 Oct 2025 16:44:21 +0200 Subject: [PATCH 10/29] fix checkstyle Signed-off-by: Pablo Carle --- .../java/org/zowe/apiml/zaas/controllers/AuthControllerTest.java | 1 - 1 file changed, 1 deletion(-) 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 1ea8de5d82..89f10e1869 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,7 +11,6 @@ package org.zowe.apiml.zaas.controllers; import com.fasterxml.jackson.databind.ObjectMapper; -import com.nimbusds.jose.jwk.JWKSet; import org.jose4j.jwk.JsonWebKey; import org.jose4j.jwk.JsonWebKeySet; import org.jose4j.lang.JoseException; From 6daddd8bcea268c0e5828c895569bcae48b2b83a Mon Sep 17 00:00:00 2001 From: Pablo Carle Date: Fri, 17 Oct 2025 10:18:14 +0200 Subject: [PATCH 11/29] fix some jwk tests Signed-off-by: Pablo Carle --- .../service/token/HttpsJwksProvider.java | 23 ++++ .../security/service/token/JWKResolver.java | 32 +++++ .../service/token/OIDCTokenProvider.java | 16 +-- .../zaas/controllers/AuthControllerTest.java | 2 +- .../zaas/security/service/JwtUtilsTest.java | 9 +- .../service/token/JWKResolverTest.java | 105 ++++++++++++++++ .../service/token/OIDCTokenProviderTest.java | 115 ++++-------------- .../service/zosmf/ZosmfServiceTest.java | 6 - .../org/zowe/apiml/zaas/utils/JWTUtils.java | 13 +- 9 files changed, 197 insertions(+), 124 deletions(-) create mode 100644 zaas-service/src/main/java/org/zowe/apiml/zaas/security/service/token/HttpsJwksProvider.java create mode 100644 zaas-service/src/main/java/org/zowe/apiml/zaas/security/service/token/JWKResolver.java create mode 100644 zaas-service/src/test/java/org/zowe/apiml/zaas/security/service/token/JWKResolverTest.java 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..97433c005a --- /dev/null +++ b/zaas-service/src/main/java/org/zowe/apiml/zaas/security/service/token/JWKResolver.java @@ -0,0 +1,32 @@ +/* + * 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); + var keySet = new JsonWebKeySet(httpsJwks.getJsonWebKeys()); + return keySet; + } + +} 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 2162f96705..8b6af3eff9 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 @@ -11,8 +11,7 @@ package org.zowe.apiml.zaas.security.service.token; import com.nimbusds.jose.JOSEException; -import com.nimbusds.jose.JWSHeader; -import com.nimbusds.jose.crypto.factories.DefaultJWSVerifierFactory; +import com.nimbusds.jose.crypto.RSASSAVerifier; import com.nimbusds.jose.proc.BadJOSEException; import com.nimbusds.jwt.JWTClaimsSet; import com.nimbusds.jwt.JWTParser; @@ -25,7 +24,6 @@ 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.HttpsJwks; import org.jose4j.jwk.JsonWebKey; import org.jose4j.jwk.JsonWebKeySet; import org.jose4j.lang.JoseException; @@ -40,6 +38,7 @@ import org.zowe.apiml.security.common.token.OIDCProvider; import java.io.IOException; +import java.security.interfaces.RSAPublicKey; import java.text.ParseException; import java.time.Clock; import java.util.List; @@ -66,13 +65,13 @@ public class OIDCTokenProvider implements OIDCProvider { @Value("${apiml.security.oidc.userInfo.uri}") private String endpointUrl; + private final JWKResolver jwkResolver; private final CloseableHttpClient secureHttpClientWithKeystore; @Getter private final Map publicKeys = new ConcurrentHashMap<>(); @Getter private JsonWebKeySet jwkSet; - @PostConstruct public void afterPropertiesSet() { this.fetchJWKSet(); @@ -91,9 +90,7 @@ void fetchJWKSet() { for (String url : jwksUri) { log.debug("Refreshing JWK endpoints {}", url); try { - var httpsJwks = new HttpsJwks(url); - - var keySet = new JsonWebKeySet(httpsJwks.getJsonWebKeys()); + 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()); @@ -156,9 +153,8 @@ JWTClaimsSet getClaims(String token) throws ParseException, BadJOSEException, JO log.debug("Validating the token with JWK"); var jwt = JWTParser.parse(token); if (jwt instanceof SignedJWT signedJwt) { - var header = JWSHeader.parse(signedJwt.getSignature()); - var verifier = new DefaultJWSVerifierFactory().createJWSVerifier(header, publicKeys.get(header.getKeyID()).getKey()); - var verified = signedJwt.verify(verifier); + var rsaVerifier = new RSASSAVerifier((RSAPublicKey) publicKeys.get(signedJwt.getHeader().getKeyID()).getKey()); + var verified = signedJwt.verify(rsaVerifier); if (verified) { var claims = jwt.getJWTClaimsSet(); if (claims.getExpirationTime().toInstant().isBefore(clock.instant())) { 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 89f10e1869..d88ce94d72 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 @@ -219,7 +219,7 @@ void useZosmf() throws Exception { 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())); } } diff --git a/zaas-service/src/test/java/org/zowe/apiml/zaas/security/service/JwtUtilsTest.java b/zaas-service/src/test/java/org/zowe/apiml/zaas/security/service/JwtUtilsTest.java index ce0995bde8..ed0a62bc35 100644 --- a/zaas-service/src/test/java/org/zowe/apiml/zaas/security/service/JwtUtilsTest.java +++ b/zaas-service/src/test/java/org/zowe/apiml/zaas/security/service/JwtUtilsTest.java @@ -10,9 +10,7 @@ package org.zowe.apiml.zaas.security.service; -import io.jsonwebtoken.Claims; -import io.jsonwebtoken.ExpiredJwtException; -import io.jsonwebtoken.Header; +import com.nimbusds.jwt.proc.ExpiredJWTException; import io.jsonwebtoken.JwtException; import org.hamcrest.Matchers; import org.junit.jupiter.api.Test; @@ -32,7 +30,6 @@ import static org.junit.jupiter.api.Assertions.assertEquals; 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; class JwtUtilsTest { @@ -41,15 +38,13 @@ class JwtUtilsTest { @Test void testHandleJwtParserExceptionForExpiredToken() { - - Exception exception = JwtUtils.handleJwtParserException(new ExpiredJwtException(mock(Header.class), mock(Claims.class), "msg")); + Exception exception = JwtUtils.handleJwtParserException(new ExpiredJWTException("msg")); assertTrue(exception instanceof TokenExpireException); assertEquals("Token is expired.", exception.getMessage()); } @Test void testHandleJwtParserExceptionForInvalidToken() { - Exception exception = JwtUtils.handleJwtParserException(new JwtException("msg")); assertTrue(exception instanceof TokenNotValidException); assertEquals("Token is not valid.", exception.getMessage()); 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..3312e7635b --- /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) +public 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 httpsJwks = new HttpsJwks(url); + httpsJwks.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(httpsJwks); + 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 httpsJwks = new HttpsJwks(url); + httpsJwks.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(httpsJwks); + 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 58bc1dbde4..e5d455520b 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,16 +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 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; @@ -34,14 +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,10 +44,7 @@ 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.ArgumentMatchers.eq; 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; @@ -60,20 +52,18 @@ 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 +77,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))) @@ -98,9 +88,8 @@ static void init() throws Exception { @BeforeEach void setup() throws CachingServiceClientException, IOException { - oidcTokenProvider = new OIDCTokenProvider(Clock.systemUTC(), httpClient); + 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 +99,39 @@ class GivenInitializationWithJwks { void whenUriNotProvided_thenNotInitialized() throws Exception { ReflectionTestUtils.setField(oidcTokenProvider, "jwksUri", Collections.emptyList()); oidcTokenProvider.afterPropertiesSet(); - verify(resourceRetriever, times(0)).retrieveResource(any()); } + + @Test + void shouldNotModifyJwksUri() throws IOException { + assertDoesNotThrow(() -> oidcTokenProvider.fetchJWKSet()); + assertTrue(oidcTokenProvider.getPublicKeys().isEmpty()); + } + } @Nested class GivenCorrectConfiguration { - @Nested class WhenJWKValidation { @BeforeEach void init() throws Exception { 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 { + when(jwkResolver.resolve("https://localjwk")).thenReturn(localJwkSet); + 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 +176,4 @@ void whenValidToken_thenReturnValid() { } } - - @Nested - class JwksUriLoad { - - @BeforeEach - public void setUp() { - oidcTokenProvider = new OIDCTokenProvider(Clock.systemUTC(), 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 bd40d0792d..fda57a62ea 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,8 +16,6 @@ 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.json.JSONException; import org.json.JSONObject; @@ -719,10 +717,6 @@ void thenSuccess() throws JSONException, IOException { 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); } diff --git a/zaas-service/src/test/java/org/zowe/apiml/zaas/utils/JWTUtils.java b/zaas-service/src/test/java/org/zowe/apiml/zaas/utils/JWTUtils.java index 4021098b8b..55a8f39f8e 100644 --- a/zaas-service/src/test/java/org/zowe/apiml/zaas/utils/JWTUtils.java +++ b/zaas-service/src/test/java/org/zowe/apiml/zaas/utils/JWTUtils.java @@ -10,10 +10,10 @@ package org.zowe.apiml.zaas.utils; -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; @@ -60,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) @@ -91,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) { } } From 04936d2da278223b979752562ec07a3b40147031 Mon Sep 17 00:00:00 2001 From: Pablo Carle Date: Fri, 17 Oct 2025 10:33:25 +0200 Subject: [PATCH 12/29] fix authenticationservice test Signed-off-by: Pablo Carle --- .../zaas/security/service/AuthenticationService.java | 12 ++++++------ .../security/service/AuthenticationServiceTest.java | 4 +++- .../security/service/zosmf/ZosmfServiceTest.java | 2 +- 3 files changed, 10 insertions(+), 8 deletions(-) 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 51f6ecbdb5..8fe5d211ac 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 @@ -14,12 +14,12 @@ import com.netflix.discovery.EurekaClient; import com.netflix.discovery.shared.Application; import com.nimbusds.jose.JOSEException; -import com.nimbusds.jose.JWSHeader; -import com.nimbusds.jose.crypto.factories.DefaultJWSVerifierFactory; +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; @@ -61,6 +61,7 @@ import org.zowe.apiml.zaas.security.service.schema.source.AuthSource; import org.zowe.apiml.zaas.security.service.zosmf.ZosmfService; +import java.security.interfaces.RSAPublicKey; import java.text.ParseException; import java.time.Clock; import java.util.Arrays; @@ -297,14 +298,13 @@ private JWTClaimsSet validateAndParseLocalJwtToken(String jwtToken) { try { var parsedJwt = JWTParser.parse(jwtToken); if (parsedJwt instanceof SignedJWT signedJwt) { - var header = JWSHeader.parse(signedJwt.getSignature()); - var verifier = new DefaultJWSVerifierFactory().createJWSVerifier(header, jwtSecurityInitializer.getJwtPublicKey()); - var verified = signedJwt.verify(verifier); + 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"); - return null; + throw new ExpiredJWTException("OIDC Token is expired"); } return claims; } 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 159eedf7b6..33e8d04b81 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 @@ -60,6 +60,7 @@ import java.security.PublicKey; import java.text.ParseException; import java.time.Clock; +import java.time.Instant; import java.util.ArrayList; import java.util.Collections; import java.util.Date; @@ -382,8 +383,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) 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 fda57a62ea..7d20107bd5 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 @@ -718,7 +718,7 @@ void thenSuccess() throws JSONException, IOException { when(authConfigurationProperties.getZosmf().getJwtEndpoint()).thenReturn(zosmfJwtUrl); ZosmfService zosmfService = getZosmfServiceSpy(); - JSONAssert.assertEquals(ZOSMF_PUBLIC_KEY_JSON, new JSONObject(zosmfService.getPublicKeys().toString()), true); + JSONAssert.assertEquals(ZOSMF_PUBLIC_KEY_JSON, new JSONObject(zosmfService.getPublicKeys().toJson()), true); } @Test From ccc1d9d0670fefafe6b79e4b9487d42c6e5e5625 Mon Sep 17 00:00:00 2001 From: Pablo Carle Date: Fri, 17 Oct 2025 10:59:18 +0200 Subject: [PATCH 13/29] fix couple test Signed-off-by: Pablo Carle --- .../apiml/zaas/security/service/AuthenticationService.java | 4 +++- .../zaas/security/service/AuthenticationServiceTest.java | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) 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 8fe5d211ac..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 @@ -152,7 +152,9 @@ private String createJWT(String username, String issuer, Map cla newClaims.setGeneratedJwtId(); newClaims.setIssuedAt(NumericDate.fromMilliseconds(issuedAt)); newClaims.setSubject(username); - newClaims.getClaimsMap().putAll(claims); + if (claims != null) { + claims.entrySet().forEach(entry -> newClaims.setClaim(entry.getKey(), entry.getValue())); + } var jws = new JsonWebSignature(); jws.setPayload(newClaims.toJson()); 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 33e8d04b81..31132a85c4 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 @@ -79,6 +79,7 @@ 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; @@ -144,6 +145,7 @@ void setup() { scopes.add("Service1"); scopes.add("Service2"); ReflectionTestUtils.setField(authService, "meAsProxy", authService); + lenient().when(clock.instant()).thenReturn(Instant.now()); } @Nested @@ -555,7 +557,7 @@ void stubJWTSecurityForSignAndVerify() { } void stubJWTSecurityForSign() { - when(jwtSecurityInitializer.getSignatureAlgorithm()).thenReturn(ALGORITHM); + lenient().when(jwtSecurityInitializer.getSignatureAlgorithm()).thenReturn(ALGORITHM); when(jwtSecurityInitializer.getJwtSecret()).thenReturn(privateKey); } From 319cfc3f5f903b3a3e7939c6eff4098f8491767c Mon Sep 17 00:00:00 2001 From: nxhafa Date: Tue, 21 Oct 2025 10:21:01 +0200 Subject: [PATCH 14/29] adding custom jvm security provider list Signed-off-by: nxhafa --- apiml-common-lib-package/build.gradle | 1 + .../resources/jvm.security.override.properties | 16 ++++++++++++++++ 2 files changed, 17 insertions(+) create mode 100644 apiml-common-lib-package/src/main/resources/jvm.security.override.properties 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 From 10e1451f371af96be7a7edebd3518bc91b3b23db Mon Sep 17 00:00:00 2001 From: nxhafa Date: Tue, 21 Oct 2025 10:26:01 +0200 Subject: [PATCH 15/29] update start.sh to override jvm security providers Signed-off-by: nxhafa --- api-catalog-package/src/main/resources/bin/start.sh | 5 ++++- apiml-package/src/main/resources/bin/start.sh | 5 ++++- caching-service-package/src/main/resources/bin/start.sh | 4 ++++ discovery-package/src/main/resources/bin/start.sh | 4 ++++ gateway-package/src/main/resources/bin/start.sh | 5 ++++- zaas-package/src/main/resources/bin/start.sh | 6 +++++- 6 files changed, 25 insertions(+), 4 deletions(-) diff --git a/api-catalog-package/src/main/resources/bin/start.sh b/api-catalog-package/src/main/resources/bin/start.sh index 2df3f1581e..cffe9b6037 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,9 @@ 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="../apiml-common-lib/bin/jvm.security.override.properties" + if [ -z "${LIBRARY_PATH}" ] then LIBRARY_PATH="../common-java-lib/bin/" @@ -323,6 +325,7 @@ _BPX_JOBNAME=${ZWE_zowe_job_prefix}${CATALOG_CODE} ${JAVA_BIN_DIR}java \ -Dserver.ssl.trustStoreType="${truststore_type}" \ -Dserver.ssl.trustStorePassword="${truststore_pass}" \ -Djava.protocol.handler.pkgs=com.ibm.crypto.provider \ + -Djava.security.properties="${JVM_SECURITY}" \ -Dloader.path=${COMMON_LIB} \ -Djava.library.path=${LIBPATH} \ -Djavax.net.debug=${ZWE_configs_sslDebug:-""} \ diff --git a/apiml-package/src/main/resources/bin/start.sh b/apiml-package/src/main/resources/bin/start.sh index 8d01710ba0..bc22db5af6 100755 --- a/apiml-package/src/main/resources/bin/start.sh +++ b/apiml-package/src/main/resources/bin/start.sh @@ -113,13 +113,15 @@ else fi echo "jar file: "${JAR_FILE} # script assumes it's in the apiml 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 apiml component directory and jvm.security.override.properties needs to be relative path +JVM_SECURITY="../apiml-common-lib/bin/jvm.security.override.properties" + if [ -z "${LIBRARY_PATH}" ]; then LIBRARY_PATH="../common-java-lib/bin/" fi @@ -414,6 +416,7 @@ _BPX_JOBNAME=${ZWE_zowe_job_prefix}${APIML_CODE} ${JAVA_BIN_DIR}java \ -Djava.io.tmpdir=${TMPDIR:-/tmp} \ -Djava.library.path=${LIBPATH} \ -Djava.protocol.handler.pkgs=com.ibm.crypto.provider \ + -Djava.security.properties="${JVM_SECURITY}" \ -Djavax.net.debug=${ZWE_configs_sslDebug:-${ZWE_components_gateway_sslDebug:-${ZWE_components_discovery_sslDebug:-""}}} \ -Djdk.tls.client.cipherSuites=${client_ciphers} \ -Djgroups.bind.address=${ZWE_components_caching_service_storage_infinispan_jgroups_host:-${ZWE_configs_storage_infinispan_jgroups_host:-${ZWE_haInstance_hostname:-localhost}}} \ diff --git a/caching-service-package/src/main/resources/bin/start.sh b/caching-service-package/src/main/resources/bin/start.sh index 487ced9b46..85144e4633 100755 --- a/caching-service-package/src/main/resources/bin/start.sh +++ b/caching-service-package/src/main/resources/bin/start.sh @@ -63,6 +63,9 @@ 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="../apiml-common-lib/bin/jvm.security.override.properties" + if [ -z "${LIBRARY_PATH}" ] then LIBRARY_PATH="../common-java-lib/bin/" @@ -289,6 +292,7 @@ _BPX_JOBNAME=${ZWE_zowe_job_prefix}${CACHING_CODE} ${JAVA_BIN_DIR}java \ -Dserver.ssl.trustStoreType="${truststore_type}" \ -Dserver.ssl.trustStorePassword="${truststore_pass}" \ -Djava.protocol.handler.pkgs=com.ibm.crypto.provider \ + -Djava.security.properties="${JVM_SECURITY}" \ -Djava.net.preferIPv4Stack=true \ -Djavax.net.debug=${ZWE_configs_sslDebug:-""} \ -Djava.library.path=${LIBPATH} \ diff --git a/discovery-package/src/main/resources/bin/start.sh b/discovery-package/src/main/resources/bin/start.sh index ac1b907856..37b811c023 100755 --- a/discovery-package/src/main/resources/bin/start.sh +++ b/discovery-package/src/main/resources/bin/start.sh @@ -65,6 +65,9 @@ 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="../apiml-common-lib/bin/jvm.security.override.properties" + if [ -z "${LIBRARY_PATH}" ]; then LIBRARY_PATH="../common-java-lib/bin/" fi @@ -282,6 +285,7 @@ _BPX_JOBNAME=${ZWE_zowe_job_prefix}${DISCOVERY_CODE} ${JAVA_BIN_DIR}java \ -Djava.io.tmpdir=${TMPDIR:-/tmp} \ -Djava.library.path=${LIBPATH} \ -Djava.protocol.handler.pkgs=com.ibm.crypto.provider \ + -Djava.security.properties="${JVM_SECURITY}" \ -Djavax.net.debug=${ZWE_configs_sslDebug:-""} \ -Djdk.tls.client.cipherSuites=${client_ciphers} \ -Dloader.path=${DISCOVERY_LOADER_PATH} \ diff --git a/gateway-package/src/main/resources/bin/start.sh b/gateway-package/src/main/resources/bin/start.sh index ddf05542e6..f90392db14 100755 --- a/gateway-package/src/main/resources/bin/start.sh +++ b/gateway-package/src/main/resources/bin/start.sh @@ -89,13 +89,15 @@ 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="../apiml-common-lib/bin/jvm.security.override.properties" + if [ -z "${LIBRARY_PATH}" ]; then LIBRARY_PATH="../common-java-lib/bin/" fi @@ -344,6 +346,7 @@ _BPX_JOBNAME=${ZWE_zowe_job_prefix}${GATEWAY_CODE} ${JAVA_BIN_DIR}java \ -Djava.io.tmpdir=${TMPDIR:-/tmp} \ -Djava.library.path=${LIBPATH} \ -Djava.protocol.handler.pkgs=com.ibm.crypto.provider \ + -Djava.security.properties="${JVM_SECURITY}" \ -Djavax.net.debug=${ZWE_configs_sslDebug:-""} \ -Djdk.tls.client.cipherSuites=${client_ciphers} \ -Dloader.path=${GATEWAY_LOADER_PATH} \ diff --git a/zaas-package/src/main/resources/bin/start.sh b/zaas-package/src/main/resources/bin/start.sh index 3892b0c99c..57596a7043 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,9 @@ 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="../apiml-common-lib/bin/jvm.security.override.properties" + if [ -z "${LIBRARY_PATH}" ] then LIBRARY_PATH="../common-java-lib/bin/" @@ -381,6 +384,7 @@ _BPX_JOBNAME=${ZWE_zowe_job_prefix}${ZAAS_CODE} ${JAVA_BIN_DIR}java \ -Dapiml.security.oidc.validationType=${ZWE_configs_apiml_security_oidc_validationType:-${ZWE_components_gateway_apiml_security_oidc_validationType:-"JWK"}} \ -Dapiml.security.allowTokenRefresh=${ZWE_configs_apiml_security_allowtokenrefresh:-${ZWE_components_gateway_apiml_security_allowtokenrefresh:-false}} \ -Djava.protocol.handler.pkgs=com.ibm.crypto.provider \ + -Djava.security.properties="${JVM_SECURITY}" \ -Dloader.path=${ZAAS_LOADER_PATH} \ -Djava.library.path=${LIBPATH} \ -Djavax.net.debug=${ZWE_configs_sslDebug:-""} \ From 09d59d4858972906ddaa1616a5e5528d3c9fcba5 Mon Sep 17 00:00:00 2001 From: nxhafa Date: Tue, 21 Oct 2025 12:08:11 +0200 Subject: [PATCH 16/29] temporary disable tests for zaas Signed-off-by: nxhafa --- .github/workflows/branch-snapshot-release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/branch-snapshot-release.yml b/.github/workflows/branch-snapshot-release.yml index 86d948a088..1aefb4ea14 100644 --- a/.github/workflows/branch-snapshot-release.yml +++ b/.github/workflows/branch-snapshot-release.yml @@ -29,7 +29,7 @@ jobs: run: | PR_NUMBER=PR-${{ env.PR_NUMBER }} sed -i '/version=/ s/-SNAPSHOT/-'"$PR_NUMBER"'-SNAPSHOT/' ./gradle.properties - ./gradlew clean build publishAllVersions -Pzowe.deploy.username=$ARTIFACTORY_USERNAME -Pzowe.deploy.password=$ARTIFACTORY_PASSWORD -Partifactory_user=$ARTIFACTORY_USERNAME -Partifactory_password=$ARTIFACTORY_USERNAME -PpullRequest=$PR_NUMBER + ./gradlew clean build -x :zaas-service:test publishAllVersions -Pzowe.deploy.username=$ARTIFACTORY_USERNAME -Pzowe.deploy.password=$ARTIFACTORY_PASSWORD -Partifactory_user=$ARTIFACTORY_USERNAME -Partifactory_password=$ARTIFACTORY_USERNAME -PpullRequest=$PR_NUMBER env: ARTIFACTORY_USERNAME: ${{ secrets.ARTIFACTORY_USERNAME }} ARTIFACTORY_PASSWORD: ${{ secrets.ARTIFACTORY_PASSWORD }} From 675e10f13af5f6d0697ea4e9b25cb9ba60dc43dd Mon Sep 17 00:00:00 2001 From: nxhafa Date: Tue, 21 Oct 2025 12:33:15 +0200 Subject: [PATCH 17/29] temporary disable tests for 'publish pax from branch' github action Signed-off-by: nxhafa --- .github/workflows/branch-snapshot-release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/branch-snapshot-release.yml b/.github/workflows/branch-snapshot-release.yml index 1aefb4ea14..ab87797b00 100644 --- a/.github/workflows/branch-snapshot-release.yml +++ b/.github/workflows/branch-snapshot-release.yml @@ -29,7 +29,7 @@ jobs: run: | PR_NUMBER=PR-${{ env.PR_NUMBER }} sed -i '/version=/ s/-SNAPSHOT/-'"$PR_NUMBER"'-SNAPSHOT/' ./gradle.properties - ./gradlew clean build -x :zaas-service:test publishAllVersions -Pzowe.deploy.username=$ARTIFACTORY_USERNAME -Pzowe.deploy.password=$ARTIFACTORY_PASSWORD -Partifactory_user=$ARTIFACTORY_USERNAME -Partifactory_password=$ARTIFACTORY_USERNAME -PpullRequest=$PR_NUMBER + ./gradlew clean build -x test publishAllVersions -Pzowe.deploy.username=$ARTIFACTORY_USERNAME -Pzowe.deploy.password=$ARTIFACTORY_PASSWORD -Partifactory_user=$ARTIFACTORY_USERNAME -Partifactory_password=$ARTIFACTORY_USERNAME -PpullRequest=$PR_NUMBER env: ARTIFACTORY_USERNAME: ${{ secrets.ARTIFACTORY_USERNAME }} ARTIFACTORY_PASSWORD: ${{ secrets.ARTIFACTORY_PASSWORD }} From 1f3f10a421680057b96ef990a9b54546a12ad594 Mon Sep 17 00:00:00 2001 From: Elena Kubantseva Date: Tue, 21 Oct 2025 18:47:58 +0200 Subject: [PATCH 18/29] fix some tests Signed-off-by: Elena Kubantseva --- .../apiml/security/common/util/JwtUtils.java | 2 +- .../ReactivePublicJWKController.java | 7 ++--- .../zaas/controllers/AuthController.java | 8 +++--- .../security/service/token/JWKResolver.java | 3 +-- .../security/service/zosmf/ZosmfService.java | 26 +++++++++--------- .../ZosmfAuthenticationProviderTest.java | 3 ++- .../query/SuccessfulQueryHandlerTest.java | 3 ++- .../service/AuthenticationServiceTest.java | 4 +++ .../zaas/security/service/JwtUtilsTest.java | 12 ++++----- .../service/token/OIDCTokenProviderTest.java | 3 +++ .../service/zosmf/ZosmfServiceTest.java | 27 ++++++++++++------- 11 files changed, 57 insertions(+), 41 deletions(-) diff --git a/apiml-security-common/src/main/java/org/zowe/apiml/security/common/util/JwtUtils.java b/apiml-security-common/src/main/java/org/zowe/apiml/security/common/util/JwtUtils.java index 52c612e9d3..0bd75c9e80 100644 --- a/apiml-security-common/src/main/java/org/zowe/apiml/security/common/util/JwtUtils.java +++ b/apiml-security-common/src/main/java/org/zowe/apiml/security/common/util/JwtUtils.java @@ -94,7 +94,7 @@ public RuntimeException handleJwtParserException(Exception exception) { log.debug("Token is expired."); return new TokenExpireException("Token is expired.", exception); } - if (exception instanceof BadJWTException) { + 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); } 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 3a514ca668..5aea10820b 100644 --- a/apiml/src/main/java/org/zowe/apiml/controller/ReactivePublicJWKController.java +++ b/apiml/src/main/java/org/zowe/apiml/controller/ReactivePublicJWKController.java @@ -23,6 +23,7 @@ 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; @@ -162,10 +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 = (PublicKey) publicKeys.get(0) - .getKey(); + RsaJsonWebKey jwk = (RsaJsonWebKey) JsonWebKey.Factory.newJwk(publicKeys.get(0).toJson()); + PublicKey key = jwk.getPublicKey(); return new ResponseEntity<>(getPublicKeyAsPem(key), HttpStatus.OK); - } catch (IOException 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); } 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 b136e7ee33..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 @@ -30,6 +30,8 @@ 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; @@ -435,10 +437,10 @@ public ResponseEntity getPublicKeyUsedForSigning() { return new ResponseEntity<>(messageService.createMessage("org.zowe.apiml.zaas.keys.wrongAmount", publicKeys.size()).mapToApiMessage(), HttpStatus.INTERNAL_SERVER_ERROR); } try { - PublicKey key = (PublicKey) publicKeys.get(0) - .getKey(); + RsaJsonWebKey jwk = (RsaJsonWebKey) JsonWebKey.Factory.newJwk(publicKeys.get(0).toJson()); + PublicKey key = jwk.getPublicKey(); return new ResponseEntity<>(getPublicKeyAsPem(key), HttpStatus.OK); - } catch (IOException 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); } 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 index 97433c005a..383884f625 100644 --- 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 @@ -25,8 +25,7 @@ public class JWKResolver { public JsonWebKeySet resolve(String url) throws JoseException, IOException { var httpsJwks = provider.getFor(url); - var keySet = new JsonWebKeySet(httpsJwks.getJsonWebKeys()); - return keySet; + return new JsonWebKeySet(httpsJwks.getJsonWebKeys()); } } 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 e5ed331d8d..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 @@ -20,7 +20,6 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang.StringUtils; -import org.jose4j.jwk.HttpsJwks; import org.jose4j.jwk.JsonWebKeySet; import org.jose4j.lang.JoseException; import org.springframework.beans.factory.annotation.Qualifier; @@ -55,9 +54,9 @@ 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; @@ -125,17 +124,19 @@ public static class ZosmfInfo { } private final List tokenValidationStrategy; - + private final AuthenticationService authenticationService; + private final JWKResolver jwkResolver; private ZosmfService meAsProxy; private TokenCreationService tokenCreationService; public ZosmfService( - final AuthConfigurationProperties authConfigurationProperties, - final @Qualifier("restTemplateWithoutKeystore") RestTemplate restTemplateWithoutKeystore, - final ObjectMapper securityObjectMapper, - final ApplicationContext applicationContext, - final AuthenticationService authenticationService, - List tokenValidationStrategy + final AuthConfigurationProperties authConfigurationProperties, + final @Qualifier("restTemplateWithoutKeystore") RestTemplate restTemplateWithoutKeystore, + final ObjectMapper securityObjectMapper, + final ApplicationContext applicationContext, + final AuthenticationService authenticationService, + List tokenValidationStrategy, + JWKResolver jwkResolver ) { super( applicationContext, @@ -145,10 +146,9 @@ public ZosmfService( ); this.tokenValidationStrategy = tokenValidationStrategy; this.authenticationService = authenticationService; + this.jwkResolver = jwkResolver; } - private final AuthenticationService authenticationService; - @PostConstruct @Override public void afterPropertiesSet() { @@ -560,10 +560,8 @@ protected ZosmfService.AuthenticationResponse getAuthenticationResponse(Response public JsonWebKeySet getPublicKeys() { var jwkZosmfUrl = getURI(getZosmfServiceId(), authConfigurationProperties.getZosmf().getJwtEndpoint()); - var httpsJwks = new HttpsJwks(jwkZosmfUrl); - try { - return new JsonWebKeySet(httpsJwks.getJsonWebKeys()); + 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()); diff --git a/zaas-service/src/test/java/org/zowe/apiml/zaas/security/login/zosmf/ZosmfAuthenticationProviderTest.java b/zaas-service/src/test/java/org/zowe/apiml/zaas/security/login/zosmf/ZosmfAuthenticationProviderTest.java index 3d0fc00804..186a1e1764 100644 --- a/zaas-service/src/test/java/org/zowe/apiml/zaas/security/login/zosmf/ZosmfAuthenticationProviderTest.java +++ b/zaas-service/src/test/java/org/zowe/apiml/zaas/security/login/zosmf/ZosmfAuthenticationProviderTest.java @@ -107,7 +107,8 @@ private ZosmfService createZosmfService() { securityObjectMapper, applicationContext, authenticationService, - new ArrayList<>()); + new ArrayList<>(), + null); ReflectionTestUtils.setField(zosmfService, "meAsProxy", zosmfService); ReflectionTestUtils.setField(zosmfService, "discovery", new CompositeDiscoveryClient(Collections.singletonList(new EurekaDiscoveryClient(eurekaClient, clientConfig)))); ReflectionTestUtils.setField(zosmfService, "tokenCreationService", tokenCreationService); 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 8e4ed6a552..e622f45314 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 @@ -102,7 +102,8 @@ void setup() { new ObjectMapper(), applicationContext, authenticationService, - new ArrayList<>()); + new ArrayList<>(), + null); ReflectionTestUtils.setField(zosmfService, "meAsProxy", zosmfService); ReflectionTestUtils.setField(zosmfService, "discovery", discoveryClient); ReflectionTestUtils.setField(zosmfService, "tokenCreationService", tokenCreationService); 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 31132a85c4..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 @@ -581,6 +581,9 @@ class GivenCacheJWTTest { @MockitoBean(name = "restTemplateWithKeystore") private RestTemplate restTemplateWithKeystore; + @MockitoBean + private Clock clock; + @Autowired private AuthenticationService authService; @@ -592,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/JwtUtilsTest.java b/zaas-service/src/test/java/org/zowe/apiml/zaas/security/service/JwtUtilsTest.java index ed0a62bc35..d1a9c2243c 100644 --- a/zaas-service/src/test/java/org/zowe/apiml/zaas/security/service/JwtUtilsTest.java +++ b/zaas-service/src/test/java/org/zowe/apiml/zaas/security/service/JwtUtilsTest.java @@ -10,8 +10,8 @@ package org.zowe.apiml.zaas.security.service; +import com.nimbusds.jwt.proc.BadJWTException; import com.nimbusds.jwt.proc.ExpiredJWTException; -import io.jsonwebtoken.JwtException; import org.hamcrest.Matchers; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; @@ -28,8 +28,8 @@ 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.zowe.apiml.zaas.utils.JWTUtils.createTokenWithUserFields; class JwtUtilsTest { @@ -39,21 +39,21 @@ class JwtUtilsTest { @Test void testHandleJwtParserExceptionForExpiredToken() { Exception exception = JwtUtils.handleJwtParserException(new ExpiredJWTException("msg")); - assertTrue(exception instanceof TokenExpireException); + 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/security/service/token/OIDCTokenProviderTest.java b/zaas-service/src/test/java/org/zowe/apiml/zaas/security/service/token/OIDCTokenProviderTest.java index e5d455520b..4615bc8106 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 @@ -45,6 +45,8 @@ 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; import static org.zowe.apiml.zaas.utils.JWTUtils.loadPrivateKey; @@ -99,6 +101,7 @@ class GivenInitializationWithJwks { void whenUriNotProvided_thenNotInitialized() throws Exception { ReflectionTestUtils.setField(oidcTokenProvider, "jwksUri", Collections.emptyList()); oidcTokenProvider.afterPropertiesSet(); + verify(jwkResolver, times(0)).resolve(any()); } @Test 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 7d20107bd5..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 @@ -17,6 +17,8 @@ import ch.qos.logback.core.Appender; import com.fasterxml.jackson.databind.ObjectMapper; 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; @@ -58,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; @@ -106,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<>(); @@ -139,7 +138,8 @@ private ZosmfService getZosmfServiceSpy() { securityObjectMapper, applicationContext, authenticationService, - null); + null, + jwkResolver); ZosmfService zosmfService = spy(zosmfServiceObj); doReturn(ZOSMF_ID).when(zosmfService).getZosmfServiceId(); doReturn("http://zosmf:1433").when(zosmfService).getURI(ZOSMF_ID); @@ -153,7 +153,8 @@ private ZosmfService getZosmfServiceWithValidationStrategy(List Date: Wed, 22 Oct 2025 11:40:18 +0200 Subject: [PATCH 19/29] remove temporary change in 'publish snapshot from branch' github action Signed-off-by: nxhafa --- .github/workflows/branch-snapshot-release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/branch-snapshot-release.yml b/.github/workflows/branch-snapshot-release.yml index ab87797b00..86d948a088 100644 --- a/.github/workflows/branch-snapshot-release.yml +++ b/.github/workflows/branch-snapshot-release.yml @@ -29,7 +29,7 @@ jobs: run: | PR_NUMBER=PR-${{ env.PR_NUMBER }} sed -i '/version=/ s/-SNAPSHOT/-'"$PR_NUMBER"'-SNAPSHOT/' ./gradle.properties - ./gradlew clean build -x test publishAllVersions -Pzowe.deploy.username=$ARTIFACTORY_USERNAME -Pzowe.deploy.password=$ARTIFACTORY_PASSWORD -Partifactory_user=$ARTIFACTORY_USERNAME -Partifactory_password=$ARTIFACTORY_USERNAME -PpullRequest=$PR_NUMBER + ./gradlew clean build publishAllVersions -Pzowe.deploy.username=$ARTIFACTORY_USERNAME -Pzowe.deploy.password=$ARTIFACTORY_PASSWORD -Partifactory_user=$ARTIFACTORY_USERNAME -Partifactory_password=$ARTIFACTORY_USERNAME -PpullRequest=$PR_NUMBER env: ARTIFACTORY_USERNAME: ${{ secrets.ARTIFACTORY_USERNAME }} ARTIFACTORY_PASSWORD: ${{ secrets.ARTIFACTORY_PASSWORD }} From e4b669ab58a4828922ad5ae7bbaa7109ab23299d Mon Sep 17 00:00:00 2001 From: Elena Kubantseva Date: Wed, 22 Oct 2025 15:58:45 +0200 Subject: [PATCH 20/29] fix last tests Signed-off-by: Elena Kubantseva --- .../ReactivePublicJWKControllerTest.java | 48 ++++++++----------- .../query/SuccessfulQueryHandlerTest.java | 3 +- 2 files changed, 23 insertions(+), 28 deletions(-) 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 666f6f0f5f..b784876a06 100644 --- a/apiml/src/test/java/org/zowe/apiml/controller/ReactivePublicJWKControllerTest.java +++ b/apiml/src/test/java/org/zowe/apiml/controller/ReactivePublicJWKControllerTest.java @@ -13,8 +13,6 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; -import com.nimbusds.jose.JOSEException; -import com.nimbusds.jose.jwk.RSAKey; import org.jose4j.jwk.JsonWebKey; import org.jose4j.jwk.JsonWebKeySet; import org.junit.jupiter.api.Test; @@ -35,6 +33,7 @@ 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; @@ -47,13 +46,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.fail; -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.mockito.Mockito.*; @ExtendWith(MockitoExtension.class) class ReactivePublicJWKControllerTest { @@ -70,12 +63,12 @@ class ReactivePublicJWKControllerTest { @Test void getAllPublicKeys_zosmfProducer_withOidc() throws Exception { - var zosmfJwk = JsonWebKey.Factory.newJwk((RSAPublicKey) generateKeyPair().getPublic()); + var zosmfJwk = JsonWebKey.Factory.newJwk(generateKeyPair().getPublic()); zosmfJwk.setKeyId("zosmfKey"); var zosmfKeySet = new JsonWebKeySet(zosmfJwk); - var apimlJwk = JsonWebKey.Factory.newJwk((RSAPublicKey) generateKeyPair().getPublic()); + var apimlJwk = JsonWebKey.Factory.newJwk(generateKeyPair().getPublic()); apimlJwk.setKeyId("apimlKey"); - var oidcJwk = JsonWebKey.Factory.newJwk((RSAPublicKey) generateKeyPair().getPublic()); + var oidcJwk = JsonWebKey.Factory.newJwk(generateKeyPair().getPublic()); oidcJwk.setKeyId("oidcKey"); var oidcKeySet = new JsonWebKeySet(oidcJwk); @@ -114,7 +107,7 @@ void getAllPublicKeys_zosmfProducer_withOidc() throws Exception { @Test void getAllPublicKeys_apimlProducer_noOidc() throws Exception { - var apimlJwk = JsonWebKey.Factory.newJwk((RSAPublicKey) generateKeyPair().getPublic()); + var apimlJwk = JsonWebKey.Factory.newJwk(generateKeyPair().getPublic()); apimlJwk.setKeyId("apimlKey"); var testControllerNoOidc = new ReactivePublicJWKController(null, jwtSecurity, zosmfService, messageService); @@ -144,7 +137,7 @@ void getAllPublicKeys_apimlProducer_noOidc() throws Exception { @Test void getCurrentPublicKeys_apimlProducer() throws Exception { - var apimlJwk = JsonWebKey.Factory.newJwk((RSAPublicKey) generateKeyPair().getPublic()); + var apimlJwk = JsonWebKey.Factory.newJwk(generateKeyPair().getPublic()); apimlJwk.setKeyId("currentApimlKey"); var apimlKeySet = new JsonWebKeySet(apimlJwk); @@ -171,7 +164,7 @@ void getCurrentPublicKeys_apimlProducer() throws Exception { @Test void getCurrentPublicKeys_zosmfProducer() throws Exception { - var zosmfJwk = JsonWebKey.Factory.newJwk((RSAPublicKey) generateKeyPair().getPublic()); + var zosmfJwk = JsonWebKey.Factory.newJwk(generateKeyPair().getPublic()); zosmfJwk.setKeyId("currentZosmfKey"); var zosmfKeySet = new JsonWebKeySet(zosmfJwk); @@ -221,7 +214,7 @@ void getCurrentPublicKeys_unknownProducer() { @Test void getPublicKeyUsedForSigning_success() throws Exception { var keyPair = generateKeyPair(); - var jwk = JsonWebKey.Factory.newJwk((RSAPublicKey) keyPair.getPublic()); + var jwk = JsonWebKey.Factory.newJwk(keyPair.getPublic()); jwk.setKeyId("signingKey"); var keySet = new JsonWebKeySet(jwk); @@ -258,9 +251,9 @@ void givenMultipleKeys_thenReturnErrorWithCorrectMessage() throws Exception { var kp1 = generateKeyPair(); var kp2 = generateKeyPair(); - var jwk1 = JsonWebKey.Factory.newJwk((RSAPublicKey) kp1.getPublic()); + var jwk1 = JsonWebKey.Factory.newJwk(kp1.getPublic()); jwk1.setKeyId("key1"); - var jwk2 = JsonWebKey.Factory.newJwk((RSAPublicKey) kp2.getPublic()); + var jwk2 = JsonWebKey.Factory.newJwk(kp2.getPublic()); jwk2.setKeyId("key2"); var keySet = new JsonWebKeySet(List.of(jwk1, jwk2)); @@ -288,19 +281,20 @@ void givenMultipleKeys_thenReturnErrorWithCorrectMessage() throws Exception { } @Test - void getPublicKeyUsedForSigning_joseException() throws Exception { - var keyPair = KeyPairGenerator.getInstance("RSA").generateKeyPair(); - var realRsaKey = new RSAKey.Builder((RSAPublicKey) keyPair.getPublic()).build(); - - var spyRsaKey = spy(realRsaKey); - doThrow(new JOSEException("Test JOSE Exception")).when(spyRsaKey).toPublicKey(); + void whenNewJwkThrowsException_thenReturnsInternalServerError() throws Exception { + byte[] badModulus = new byte[]{0}; - var jwk = JsonWebKey.Factory.newJwk(keyPair.getPublic()); + 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]); - var keySet = new JsonWebKeySet(List.of(jwk)); + 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); 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 e622f45314..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 @@ -45,6 +45,7 @@ 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) @@ -112,7 +113,7 @@ void setup() { applicationContext, authConfigurationProperties, jwtSecurityInitializer, zosmfService, 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); From 32d7aba0d57d8f37cd2aac5520cbdc58564ab964 Mon Sep 17 00:00:00 2001 From: nxhafa Date: Wed, 22 Oct 2025 17:56:28 +0200 Subject: [PATCH 21/29] do not run AcceptanceTests in parallel Signed-off-by: nxhafa --- .../test/java/org/zowe/apiml/acceptance/AcceptanceTest.java | 3 +++ 1 file changed, 3 insertions(+) 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 From 70fd031e0d98351c759207d5457a1c31fa2db732 Mon Sep 17 00:00:00 2001 From: Elena Kubantseva Date: Wed, 22 Oct 2025 18:38:44 +0200 Subject: [PATCH 22/29] restore changes form master Signed-off-by: Elena Kubantseva --- apiml-package/src/main/resources/bin/start.sh | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/apiml-package/src/main/resources/bin/start.sh b/apiml-package/src/main/resources/bin/start.sh index 888f97b61d..28266df901 100755 --- a/apiml-package/src/main/resources/bin/start.sh +++ b/apiml-package/src/main/resources/bin/start.sh @@ -135,7 +135,11 @@ add_profile() { ZWE_configs_spring_profiles_active="${ZWE_configs_spring_profiles_active}${new_profile}" } -if [ "${ZWE_configs_debug:-${ZWE_components_gateway_debug:-${ZWE_components_discovery_debug:-false}}}" = "true" ]; then +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 + ZWE_configs_spring_profiles_active="${ZWE_configs_spring_profiles_active:-${ZWE_components_gateway_spring_profiles_active:-${ZWE_components_discovery_spring_profiles_active}}}," + fi add_profile "debug" fi From bdbda2d04fe70cc9884dc9adb38cef2d8749e5ed Mon Sep 17 00:00:00 2001 From: Pablo Carle Date: Fri, 24 Oct 2025 16:55:39 +0200 Subject: [PATCH 23/29] add exception checks Signed-off-by: Pablo Carle --- .../zaas/security/service/token/OIDCTokenProvider.java | 9 +++++++++ 1 file changed, 9 insertions(+) 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 8b6af3eff9..93c8af172e 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 @@ -12,6 +12,7 @@ import com.nimbusds.jose.JOSEException; 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; @@ -147,12 +148,20 @@ JWTClaimsSet getClaims(String token) throws ParseException, BadJOSEException, JO fetchJWKSet(); } + if (jwkSet == null || jwkSet.getJsonWebKeys().isEmpty()) { + throw new JWKException("Could not validate the token due to missing public key."); + } + if (StringUtils.isBlank(token)) { throw new BadJOSEException("Empty string provided instead of a token."); } + log.debug("Validating the token with JWK"); var jwt = JWTParser.parse(token); if (jwt instanceof SignedJWT signedJwt) { + if (StringUtils.isBlank(signedJwt.getHeader().getKeyID())) { + throw new JWKException("Token does not provide kid. It uses an unsupported type of signature."); + } var rsaVerifier = new RSASSAVerifier((RSAPublicKey) publicKeys.get(signedJwt.getHeader().getKeyID()).getKey()); var verified = signedJwt.verify(rsaVerifier); if (verified) { From 6d11ec11db76491752f94846668bb5c8917c5fe9 Mon Sep 17 00:00:00 2001 From: Pablo Carle Date: Fri, 24 Oct 2025 17:22:34 +0200 Subject: [PATCH 24/29] add missing checks Signed-off-by: Pablo Carle --- .../service/token/OIDCTokenProvider.java | 24 ++++++++++++------- 1 file changed, 16 insertions(+), 8 deletions(-) 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 93c8af172e..0977799509 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 @@ -162,15 +162,23 @@ JWTClaimsSet getClaims(String token) throws ParseException, BadJOSEException, JO if (StringUtils.isBlank(signedJwt.getHeader().getKeyID())) { throw new JWKException("Token does not provide kid. It uses an unsupported type of signature."); } - var rsaVerifier = new RSASSAVerifier((RSAPublicKey) publicKeys.get(signedJwt.getHeader().getKeyID()).getKey()); - var verified = signedJwt.verify(rsaVerifier); - if (verified) { - var claims = jwt.getJWTClaimsSet(); - if (claims.getExpirationTime().toInstant().isBefore(clock.instant())) { - log.debug("OIDC Token is expired"); - return null; + + var jsonWebKey = publicKeys.get(signedJwt.getHeader().getKeyID()); + if (jsonWebKey != null) { + var rsaVerifier = new RSASSAVerifier((RSAPublicKey) jsonWebKey.getKey()); + var verified = signedJwt.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"); } - return claims; + } else { + throw new JWKException("Key with id " + signedJwt.getHeader().getKeyID() + " is null in JWK"); } } else { log.debug("OIDC Token is not signed"); From 47c69a64a0189f64b2983e611f28e8d82c4ba795 Mon Sep 17 00:00:00 2001 From: Pablo Carle Date: Sat, 25 Oct 2025 16:23:21 +0200 Subject: [PATCH 25/29] fix sonar issues Signed-off-by: Pablo Carle --- .../apiml/security/common/util/JwtUtils.java | 6 +- .../service/token/OIDCTokenProvider.java | 56 ++++++++++--------- .../zaas/controllers/AuthControllerTest.java | 2 +- .../service/token/JWKResolverTest.java | 12 ++-- .../service/token/OIDCTokenProviderTest.java | 7 +-- 5 files changed, 43 insertions(+), 40 deletions(-) diff --git a/apiml-security-common/src/main/java/org/zowe/apiml/security/common/util/JwtUtils.java b/apiml-security-common/src/main/java/org/zowe/apiml/security/common/util/JwtUtils.java index 0bd75c9e80..9338f2a097 100644 --- a/apiml-security-common/src/main/java/org/zowe/apiml/security/common/util/JwtUtils.java +++ b/apiml-security-common/src/main/java/org/zowe/apiml/security/common/util/JwtUtils.java @@ -153,10 +153,8 @@ private List extractNestedFields(JWTClaimsSet claims, List pathT var claim = claims.getClaim(key); while (iterator.hasNext()) { key = iterator.next(); - if (iterator.hasNext()) { - if (claim instanceof Map val) { - claim = val.get(key); - } + if (iterator.hasNext() && claim instanceof Map val) { + claim = val.get(key); } } 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 0977799509..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 @@ -44,6 +44,7 @@ import java.time.Clock; import java.util.List; import java.util.Map; +import java.util.Map.Entry; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; @@ -98,7 +99,7 @@ void fetchJWKSet() { } } - jwkSet = new JsonWebKeySet(publicKeys.entrySet().stream().map(entry -> entry.getValue()).toList()); + jwkSet = new JsonWebKeySet(publicKeys.entrySet().stream().map(Entry::getValue).toList()); } @Override @@ -144,6 +145,10 @@ public boolean isValidExternal(String token) { } 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(); } @@ -152,39 +157,40 @@ JWTClaimsSet getClaims(String token) throws ParseException, BadJOSEException, JO throw new JWKException("Could not validate the token due to missing public key."); } - if (StringUtils.isBlank(token)) { - throw new BadJOSEException("Empty string provided instead of a token."); - } - log.debug("Validating the token with JWK"); var jwt = JWTParser.parse(token); if (jwt instanceof SignedJWT signedJwt) { - if (StringUtils.isBlank(signedJwt.getHeader().getKeyID())) { - throw new JWKException("Token does not provide kid. It uses an unsupported type of signature."); - } + return getClaims(signedJwt); + } else { + log.debug("OIDC Token is not signed"); + } + return null; + + } - var jsonWebKey = publicKeys.get(signedJwt.getHeader().getKeyID()); - if (jsonWebKey != null) { - var rsaVerifier = new RSASSAVerifier((RSAPublicKey) jsonWebKey.getKey()); - var verified = signedJwt.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"); + 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."); + } + + 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 JWKException("Key with id " + signedJwt.getHeader().getKeyID() + " is null in JWK"); + throw new BadJOSEException("Provided OIDC JWT token has invalid signature"); } } else { - log.debug("OIDC Token is not signed"); + throw new JWKException("Key with id " + keyId + " is null in JWK"); } - return null; - } } 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 d88ce94d72..2a2cfd4baf 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 @@ -136,7 +136,7 @@ void distributeInvalidate() throws Exception { this.mockMvc.perform(get("/zaas/api/v1/auth/distribute/instance2")).andExpect(status().is(SC_NO_CONTENT)); } - private JsonWebKey getJwk(int i) throws ParseException, JoseException { + private JsonWebKey getJwk(int i) throws JoseException { return JsonWebKey.Factory.newJwk("{" + "\"e\":\"AQAB\"," + "\"n\":\"kWp2zRA23Z3vTL4uoe8kTFptxBVFunIoP4t_8TDYJrOb7D1iZNDXVeEsYKp6ppmrTZDAgd-cNOTKLd4M39WJc5FN0maTAVKJc7NxklDeKc4dMe1BGvTZNG4MpWBo-taKULlYUu0ltYJuLzOjIrTHfarucrGoRWqM0sl3z2-fv9k\",\n" + 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 index 3312e7635b..f228fcbb26 100644 --- 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 @@ -51,8 +51,8 @@ class JwksUriLoad { @Test void givenMissingParameterInJWK_doNotThrowException() throws IOException { var url = "https://localhost/jwk"; - var httpsJwks = new HttpsJwks(url); - httpsJwks.setSimpleHttpGet(simpleGet); + var jwks = new HttpsJwks(url); + jwks.setSimpleHttpGet(simpleGet); var json = """ { @@ -69,7 +69,7 @@ void givenMissingParameterInJWK_doNotThrowException() throws IOException { } """; - when(provider.getFor(url)).thenReturn(httpsJwks); + when(provider.getFor(url)).thenReturn(jwks); when(simpleGet.get(url)).thenReturn(new Response(200, "", Map.of(), json)); assertDoesNotThrow(() -> jwkResolver.resolve(url)); @@ -78,8 +78,8 @@ void givenMissingParameterInJWK_doNotThrowException() throws IOException { @Test void giveValidJWK_setPublicKey() throws IOException { var url = "https://localhost/jwk"; - var httpsJwks = new HttpsJwks(url); - httpsJwks.setSimpleHttpGet(simpleGet); + var jwks = new HttpsJwks(url); + jwks.setSimpleHttpGet(simpleGet); var json = """ { @@ -95,7 +95,7 @@ void giveValidJWK_setPublicKey() throws IOException { ] } """; - when(provider.getFor(url)).thenReturn(httpsJwks); + 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 4615bc8106..095ce7b3b5 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 @@ -53,7 +53,6 @@ @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"; @@ -89,7 +88,7 @@ static void init() throws Exception { } @BeforeEach - void setup() throws CachingServiceClientException, IOException { + void setup() throws CachingServiceClientException { oidcTokenProvider = new OIDCTokenProvider(Clock.systemUTC(), jwkResolver, httpClient); ReflectionTestUtils.setField(oidcTokenProvider, "jwkRefreshInterval", 1); } @@ -105,7 +104,7 @@ void whenUriNotProvided_thenNotInitialized() throws Exception { } @Test - void shouldNotModifyJwksUri() throws IOException { + void shouldNotModifyJwksUri() { assertDoesNotThrow(() -> oidcTokenProvider.fetchJWKSet()); assertTrue(oidcTokenProvider.getPublicKeys().isEmpty()); } @@ -119,7 +118,7 @@ class GivenCorrectConfiguration { class WhenJWKValidation { @BeforeEach - void init() throws Exception { + void init() { ReflectionTestUtils.setField(oidcTokenProvider, "jwksUri", Arrays.asList("https://localjwk", "https://jwksurl")); } From 7d1a1160f8c4ce14ed9a214d4b92082abd7e2647 Mon Sep 17 00:00:00 2001 From: Pablo Carle Date: Sat, 25 Oct 2025 16:39:22 +0200 Subject: [PATCH 26/29] fix unit test Signed-off-by: Pablo Carle --- .../zaas/security/service/token/OIDCTokenProviderTest.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) 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 095ce7b3b5..fcec554577 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 @@ -44,6 +44,7 @@ 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.lenient; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; @@ -125,8 +126,8 @@ void init() { @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) throws JoseException, IOException { - when(jwkResolver.resolve("https://localjwk")).thenReturn(localJwkSet); - when(jwkResolver.resolve("https://jwksurl")).thenReturn(localJwkSet); + lenient().when(jwkResolver.resolve("https://localjwk")).thenReturn(localJwkSet); + lenient().when(jwkResolver.resolve("https://jwksurl")).thenReturn(localJwkSet); assertFalse(oidcTokenProvider.isValid(token)); } From de8236a3cd045fb54a04039cc9e4957817f7d416 Mon Sep 17 00:00:00 2001 From: Pablo Carle Date: Mon, 27 Oct 2025 10:45:22 +0100 Subject: [PATCH 27/29] move jwtutilstest to apiml security common Signed-off-by: Pablo Carle --- apiml-security-common/build.gradle | 13 +++++++++++++ .../apiml/security/common/util}/JwtUtilsTest.java | 5 ++--- .../apiml/security/common/util/JWTTestUtils.java | 4 ++-- zaas-service/build.gradle | 1 + .../java/org/zowe/apiml/acceptance/ZaasTest.java | 4 ++-- .../schema/source/OIDCAuthSourceServiceTest.java | 2 +- .../service/token/OIDCTokenProviderTest.java | 2 +- 7 files changed, 22 insertions(+), 9 deletions(-) rename {zaas-service/src/test/java/org/zowe/apiml/zaas/security/service => apiml-security-common/src/test/java/org/zowe/apiml/security/common/util}/JwtUtilsTest.java (96%) rename zaas-service/src/test/java/org/zowe/apiml/zaas/utils/JWTUtils.java => apiml-security-common/src/testFixtures/java/org/zowe/apiml/security/common/util/JWTTestUtils.java (98%) diff --git a/apiml-security-common/build.gradle b/apiml-security-common/build.gradle index 7f7fee1c58..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') @@ -23,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/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 96% 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 d1a9c2243c..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,7 +8,7 @@ * Copyright Contributors to the Zowe Project. */ -package org.zowe.apiml.zaas.security.service; +package org.zowe.apiml.security.common.util; import com.nimbusds.jwt.proc.BadJWTException; import com.nimbusds.jwt.proc.ExpiredJWTException; @@ -20,7 +20,6 @@ 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.security.common.util.JwtUtils; import java.util.Arrays; import java.util.Collections; @@ -30,7 +29,7 @@ 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.zowe.apiml.zaas.utils.JWTUtils.createTokenWithUserFields; +import static org.zowe.apiml.security.common.util.JWTTestUtils.createTokenWithUserFields; class JwtUtilsTest { 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 98% 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 55a8f39f8e..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,7 +8,7 @@ * Copyright Contributors to the Zowe Project. */ -package org.zowe.apiml.zaas.utils; +package org.zowe.apiml.security.common.util; import io.jsonwebtoken.Jwts; import lombok.SneakyThrows; @@ -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"); diff --git a/zaas-service/build.gradle b/zaas-service/build.gradle index ab7593e8b9..9ea505c415 100644 --- a/zaas-service/build.gradle +++ b/zaas-service/build.gradle @@ -120,6 +120,7 @@ dependencies { testAnnotationProcessor libs.lombok testImplementation(testFixtures(project(":apiml-common"))) + testImplementation(testFixtures(project(":apiml-security-common"))) } bootJar { 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/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/OIDCTokenProviderTest.java b/zaas-service/src/test/java/org/zowe/apiml/zaas/security/service/token/OIDCTokenProviderTest.java index fcec554577..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 @@ -49,7 +49,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.loadPrivateKey; +import static org.zowe.apiml.security.common.util.JWTTestUtils.loadPrivateKey; @ExtendWith(MockitoExtension.class) class OIDCTokenProviderTest { From f4dfd89daf27961b20fe2df589a7c0382436675f Mon Sep 17 00:00:00 2001 From: Pablo Carle Date: Mon, 27 Oct 2025 11:32:06 +0100 Subject: [PATCH 28/29] fix couple issues sonar Signed-off-by: Pablo Carle --- .../org/zowe/apiml/zaas/controllers/AuthControllerTest.java | 3 +-- .../apiml/zaas/security/service/token/JWKResolverTest.java | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) 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 2a2cfd4baf..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 @@ -45,7 +45,6 @@ import java.io.IOException; import java.math.BigInteger; import java.security.interfaces.RSAPublicKey; -import java.text.ParseException; import java.util.Arrays; import java.util.Collections; import java.util.List; @@ -101,7 +100,7 @@ class AuthControllerTest { private JSONObject body; @BeforeEach - void setUp() throws ParseException, JSONException, JoseException { + 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(); 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 index f228fcbb26..035568e86c 100644 --- 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 @@ -27,7 +27,7 @@ import static org.mockito.Mockito.when; @ExtendWith(MockitoExtension.class) -public class JWKResolverTest { +class JWKResolverTest { @Mock private HttpsJwks httpsJwks; From afa2d40c1f7b293b7bd0647f74878404676c1e8d Mon Sep 17 00:00:00 2001 From: nxhafa Date: Mon, 27 Oct 2025 14:38:39 +0100 Subject: [PATCH 29/29] add env variable for overriding java security providers Signed-off-by: nxhafa --- api-catalog-package/src/main/resources/bin/start.sh | 7 +++++-- apiml-package/src/main/resources/bin/start.sh | 7 +++++-- caching-service-package/src/main/resources/bin/start.sh | 7 +++++-- discovery-package/src/main/resources/bin/start.sh | 7 +++++-- gateway-package/src/main/resources/bin/start.sh | 7 +++++-- zaas-package/src/main/resources/bin/start.sh | 7 +++++-- 6 files changed, 30 insertions(+), 12 deletions(-) diff --git a/api-catalog-package/src/main/resources/bin/start.sh b/api-catalog-package/src/main/resources/bin/start.sh index cffe9b6037..8681c7da3a 100755 --- a/api-catalog-package/src/main/resources/bin/start.sh +++ b/api-catalog-package/src/main/resources/bin/start.sh @@ -64,7 +64,10 @@ else fi # script assumes it's in the api-catalog component directory and jvm.security.override.properties needs to be relative path -JVM_SECURITY="../apiml-common-lib/bin/jvm.security.override.properties" +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 @@ -281,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} \ @@ -325,7 +329,6 @@ _BPX_JOBNAME=${ZWE_zowe_job_prefix}${CATALOG_CODE} ${JAVA_BIN_DIR}java \ -Dserver.ssl.trustStoreType="${truststore_type}" \ -Dserver.ssl.trustStorePassword="${truststore_pass}" \ -Djava.protocol.handler.pkgs=com.ibm.crypto.provider \ - -Djava.security.properties="${JVM_SECURITY}" \ -Dloader.path=${COMMON_LIB} \ -Djava.library.path=${LIBPATH} \ -Djavax.net.debug=${ZWE_configs_sslDebug:-""} \ diff --git a/apiml-package/src/main/resources/bin/start.sh b/apiml-package/src/main/resources/bin/start.sh index 28266df901..08f1282b1a 100755 --- a/apiml-package/src/main/resources/bin/start.sh +++ b/apiml-package/src/main/resources/bin/start.sh @@ -121,7 +121,10 @@ else fi # script assumes it's in the apiml component directory and jvm.security.override.properties needs to be relative path -JVM_SECURITY="../apiml-common-lib/bin/jvm.security.override.properties" +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/" @@ -332,6 +335,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:-}} \ @@ -421,7 +425,6 @@ _BPX_JOBNAME=${ZWE_zowe_job_prefix}${APIML_CODE} ${JAVA_BIN_DIR}java \ -Djava.io.tmpdir=${TMPDIR:-/tmp} \ -Djava.library.path=${LIBPATH} \ -Djava.protocol.handler.pkgs=com.ibm.crypto.provider \ - -Djava.security.properties="${JVM_SECURITY}" \ -Djavax.net.debug=${ZWE_configs_sslDebug:-${ZWE_components_gateway_sslDebug:-${ZWE_components_discovery_sslDebug:-""}}} \ -Djdk.tls.client.cipherSuites=${client_ciphers} \ -Djgroups.bind.address=${ZWE_components_caching_service_storage_infinispan_jgroups_host:-${ZWE_configs_storage_infinispan_jgroups_host:-${ZWE_haInstance_hostname:-localhost}}} \ diff --git a/caching-service-package/src/main/resources/bin/start.sh b/caching-service-package/src/main/resources/bin/start.sh index 85144e4633..7e275f8bd8 100755 --- a/caching-service-package/src/main/resources/bin/start.sh +++ b/caching-service-package/src/main/resources/bin/start.sh @@ -64,7 +64,10 @@ then fi # script assumes it's in the caching-service component directory and jvm.security.override.properties needs to be relative path -JVM_SECURITY="../apiml-common-lib/bin/jvm.security.override.properties" +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 @@ -254,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} \ @@ -292,7 +296,6 @@ _BPX_JOBNAME=${ZWE_zowe_job_prefix}${CACHING_CODE} ${JAVA_BIN_DIR}java \ -Dserver.ssl.trustStoreType="${truststore_type}" \ -Dserver.ssl.trustStorePassword="${truststore_pass}" \ -Djava.protocol.handler.pkgs=com.ibm.crypto.provider \ - -Djava.security.properties="${JVM_SECURITY}" \ -Djava.net.preferIPv4Stack=true \ -Djavax.net.debug=${ZWE_configs_sslDebug:-""} \ -Djava.library.path=${LIBPATH} \ diff --git a/discovery-package/src/main/resources/bin/start.sh b/discovery-package/src/main/resources/bin/start.sh index 37b811c023..49dfe9244d 100755 --- a/discovery-package/src/main/resources/bin/start.sh +++ b/discovery-package/src/main/resources/bin/start.sh @@ -66,7 +66,10 @@ else fi # script assumes it's in the discovery component directory and jvm.security.override.properties needs to be relative path -JVM_SECURITY="../apiml-common-lib/bin/jvm.security.override.properties" +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/" @@ -267,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} \ @@ -285,7 +289,6 @@ _BPX_JOBNAME=${ZWE_zowe_job_prefix}${DISCOVERY_CODE} ${JAVA_BIN_DIR}java \ -Djava.io.tmpdir=${TMPDIR:-/tmp} \ -Djava.library.path=${LIBPATH} \ -Djava.protocol.handler.pkgs=com.ibm.crypto.provider \ - -Djava.security.properties="${JVM_SECURITY}" \ -Djavax.net.debug=${ZWE_configs_sslDebug:-""} \ -Djdk.tls.client.cipherSuites=${client_ciphers} \ -Dloader.path=${DISCOVERY_LOADER_PATH} \ diff --git a/gateway-package/src/main/resources/bin/start.sh b/gateway-package/src/main/resources/bin/start.sh index f90392db14..d65a340581 100755 --- a/gateway-package/src/main/resources/bin/start.sh +++ b/gateway-package/src/main/resources/bin/start.sh @@ -96,7 +96,10 @@ else fi # script assumes it's in the gateway component directory and jvm.security.override.properties needs to be relative path -JVM_SECURITY="../apiml-common-lib/bin/jvm.security.override.properties" +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/" @@ -300,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} \ @@ -346,7 +350,6 @@ _BPX_JOBNAME=${ZWE_zowe_job_prefix}${GATEWAY_CODE} ${JAVA_BIN_DIR}java \ -Djava.io.tmpdir=${TMPDIR:-/tmp} \ -Djava.library.path=${LIBPATH} \ -Djava.protocol.handler.pkgs=com.ibm.crypto.provider \ - -Djava.security.properties="${JVM_SECURITY}" \ -Djavax.net.debug=${ZWE_configs_sslDebug:-""} \ -Djdk.tls.client.cipherSuites=${client_ciphers} \ -Dloader.path=${GATEWAY_LOADER_PATH} \ diff --git a/zaas-package/src/main/resources/bin/start.sh b/zaas-package/src/main/resources/bin/start.sh index 57596a7043..e6ebcc4906 100755 --- a/zaas-package/src/main/resources/bin/start.sh +++ b/zaas-package/src/main/resources/bin/start.sh @@ -91,7 +91,10 @@ else fi # script assumes it's in the zaas component directory and jvm.security.override.properties needs to be relative path -JVM_SECURITY="../apiml-common-lib/bin/jvm.security.override.properties" +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 @@ -320,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} \ @@ -384,7 +388,6 @@ _BPX_JOBNAME=${ZWE_zowe_job_prefix}${ZAAS_CODE} ${JAVA_BIN_DIR}java \ -Dapiml.security.oidc.validationType=${ZWE_configs_apiml_security_oidc_validationType:-${ZWE_components_gateway_apiml_security_oidc_validationType:-"JWK"}} \ -Dapiml.security.allowTokenRefresh=${ZWE_configs_apiml_security_allowtokenrefresh:-${ZWE_components_gateway_apiml_security_allowtokenrefresh:-false}} \ -Djava.protocol.handler.pkgs=com.ibm.crypto.provider \ - -Djava.security.properties="${JVM_SECURITY}" \ -Dloader.path=${ZAAS_LOADER_PATH} \ -Djava.library.path=${LIBPATH} \ -Djavax.net.debug=${ZWE_configs_sslDebug:-""} \