Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
06a1d45
add josejwt
Oct 10, 2025
2ced5c9
update poc
Oct 10, 2025
0292adf
update claims and jws setup
arxioly Oct 13, 2025
c07ce99
wip, compiles using nimbus for parsing and jose4j for signing
Oct 15, 2025
3461a55
test for nimbus signing
Oct 16, 2025
c045085
fix test compile issues
Oct 16, 2025
ab98725
fix checkstyle
Oct 16, 2025
cc61763
fix test classpath
Oct 16, 2025
c1a8a31
fix few tests
Oct 16, 2025
ab310cd
fix checkstyle
Oct 16, 2025
6daddd8
fix some jwk tests
Oct 17, 2025
04936d2
fix authenticationservice test
Oct 17, 2025
e058e96
Merge remote-tracking branch 'origin/v3.x.x' into reboot/jwt-hw-poc
Oct 17, 2025
ccc1d9d
fix couple test
Oct 17, 2025
319cfc3
adding custom jvm security provider list
nxhafa Oct 21, 2025
10e1451
update start.sh to override jvm security providers
nxhafa Oct 21, 2025
09d59d4
temporary disable tests for zaas
nxhafa Oct 21, 2025
675e10f
temporary disable tests for 'publish pax from branch' github action
nxhafa Oct 21, 2025
1f3f10a
fix some tests
arxioly Oct 21, 2025
0860731
remove temporary change in 'publish snapshot from branch' github action
nxhafa Oct 22, 2025
e4b669a
fix last tests
arxioly Oct 22, 2025
32d7aba
do not run AcceptanceTests in parallel
nxhafa Oct 22, 2025
6fb3dcf
Merge branch 'refs/heads/v3.x.x' into reboot/jwt-hw-poc
arxioly Oct 22, 2025
70fd031
restore changes form master
arxioly Oct 22, 2025
0adbb9f
Merge branch 'v3.x.x' into reboot/jwt-hw-poc
pablocarle Oct 24, 2025
bdbda2d
add exception checks
Oct 24, 2025
6d11ec1
add missing checks
Oct 24, 2025
47c69a6
fix sonar issues
Oct 25, 2025
7d1a116
fix unit test
Oct 25, 2025
de8236a
move jwtutilstest to apiml security common
Oct 27, 2025
f4dfd89
fix couple issues sonar
Oct 27, 2025
afa2d40
add env variable for overriding java security providers
nxhafa Oct 27, 2025
c7a45e9
Merge branch 'v3.x.x' into reboot/jwt-hw-poc
pablocarle Oct 27, 2025
48f3f22
Merge branch 'v3.x.x' into reboot/jwt-hw-poc
pablocarle Oct 29, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion api-catalog-package/src/main/resources/bin/start.sh
Original file line number Diff line number Diff line change
Expand Up @@ -56,14 +56,19 @@ 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"
else
COMMON_LIB=${CMMN_LB}
fi

# script assumes it's in the api-catalog component directory and jvm.security.override.properties needs to be relative path
JVM_SECURITY_PROPERTIES=""
if [ "${JVM_SECURITY_PROPERTIES_OVERRIDE:-false}" = "true" ]; then
JVM_SECURITY_PROPERTIES="-Djava.security.properties=../apiml-common-lib/bin/jvm.security.override.properties"
fi

if [ -z "${LIBRARY_PATH}" ]
then
LIBRARY_PATH="../common-java-lib/bin/"
Expand Down Expand Up @@ -279,6 +284,7 @@ _BPX_JOBNAME=${ZWE_zowe_job_prefix}${CATALOG_CODE} ${JAVA_BIN_DIR}java \
${QUICK_START} \
${ADD_OPENS} \
${LOGBACK} \
${JVM_SECURITY_PROPERTIES} \
-Dibm.serversocket.recover=true \
-Dfile.encoding=UTF-8 \
-Dlogging.charset.console=${ZOWE_CONSOLE_LOG_CHARSET} \
Expand Down
1 change: 1 addition & 0 deletions apiml-common-lib-package/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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
17 changes: 16 additions & 1 deletion apiml-package/src/main/resources/bin/start.sh
Original file line number Diff line number Diff line change
Expand Up @@ -128,10 +128,24 @@ else
COMMON_LIB="${CMMN_LB}"
fi

# script assumes it's in the apiml component directory and jvm.security.override.properties needs to be relative path
JVM_SECURITY_PROPERTIES=""
if [ "${JVM_SECURITY_PROPERTIES_OVERRIDE:-false}" = "true" ]; then
JVM_SECURITY_PROPERTIES="-Djava.security.properties=../apiml-common-lib/bin/jvm.security.override.properties"
fi

if [ -z "${LIBRARY_PATH}" ]; then
LIBRARY_PATH="../common-java-lib/bin/"
fi

add_profile() {
new_profile=$1
if [ -n "${ZWE_configs_spring_profiles_active}" ]; then
ZWE_configs_spring_profiles_active="${ZWE_configs_spring_profiles_active},"
fi
ZWE_configs_spring_profiles_active="${ZWE_configs_spring_profiles_active}${new_profile}"
}

if [ "${ZWE_components_gateway_debug:-${ZWE_configs_debug:-false}}" = "true" ]; then
# TODO should this be a merge of the profiles in gateway and discovery (and other modules later added?)
if [ -n "${ZWE_configs_spring_profiles_active:-${ZWE_components_gateway_spring_profiles_active:-${ZWE_components_discovery_spring_profiles_active}}}" ]; then
Expand Down Expand Up @@ -329,6 +343,7 @@ _BPX_JOBNAME=${ZWE_zowe_job_prefix}${APIML_CODE} ${JAVA_BIN_DIR}java \
${QUICK_START} \
${ADD_OPENS} \
${LOGBACK} \
${JVM_SECURITY_PROPERTIES} \
-Dapiml.cache.storage.location=${ZWE_zowe_workspaceDirectory}/api-mediation/${ZWE_haInstance_id:-localhost} \
-Dapiml.catalog.customStyle.backgroundColor=${ZWE_components_apicatalog_apiml_catalog_customStyle_backgroundColor:-${ZWE_configs_apiml_catalog_customStyle_backgroundColor:-}} \
-Dapiml.catalog.customStyle.docLink=${ZWE_components_apicatalog_apiml_catalog_customStyle_docLink:-${ZWE_configs_apiml_catalog_customStyle_docLink:-}} \
Expand Down Expand Up @@ -401,7 +416,7 @@ _BPX_JOBNAME=${ZWE_zowe_job_prefix}${APIML_CODE} ${JAVA_BIN_DIR}java \
-Dapiml.service.allowEncodedSlashes=${ZWE_components_gateway_apiml_service_allowEncodedSlashes:-${ZWE_configs_apiml_service_allowEncodedSlashes:-true}} \
-Dapiml.service.apimlId=${ZWE_components_gateway_apimlId:-${ZWE_configs_apimlId:-}} \
-Dapiml.service.corsEnabled=${ZWE_components_gateway_apiml_service_corsEnabled:-${ZWE_configs_apiml_service_corsEnabled:-false}} \
-Dapiml.service.corsAllowedMethods=${ZWE_components_gateway_apiml_service_corsAllowedMethods:-${ZWE_configs_apiml_service_corsAllowedMethods:-}} \
-Dapiml.service.corsAllowedMethods=${ZWE_components_gateway_apiml_service_corsAllowedMethods:-${ZWE_configs_apiml_service_corsAllowedMethods:-GET,HEAD,POST,PATCH,DELETE,PUT,OPTIONS}} \
-Dapiml.service.externalUrl="${externalProtocol}://${ZWE_zowe_externalDomains_0}:${ZWE_zowe_externalPort}" \
-Dapiml.service.forwardClientCertEnabled=${ZWE_components_gateway_apiml_security_x509_enabled:-${ZWE_configs_apiml_security_x509_enabled:-false}} \
-Dapiml.service.hostname=${ZWE_haInstance_hostname:-localhost} \
Expand Down
15 changes: 15 additions & 0 deletions apiml-security-common/build.gradle
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
plugins {
id "java-test-fixtures"
}

dependencies {
api project(':apiml-common')

Expand All @@ -12,6 +16,8 @@ dependencies {
implementation libs.apache.commons.lang3
implementation libs.http.client5

implementation libs.nimbus.jose.jwt // Parsing

testImplementation libs.spring.boot.starter.test
testImplementation(testFixtures(project(":apiml-common")))

Expand All @@ -21,4 +27,13 @@ dependencies {
testCompileOnly libs.lombok
testImplementation libs.netty.reactor.http
testAnnotationProcessor libs.lombok

testFixturesImplementation libs.nimbus.jose.jwt
testFixturesImplementation libs.jose4j.jwt
testFixturesImplementation libs.jjwt
testFixturesImplementation libs.jjwt.impl
testFixturesImplementation libs.jjwt.jackson

testFixturesCompileOnly libs.lombok
testFixturesAnnotationProcessor libs.lombok
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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);
}
}
Expand All @@ -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);
}
Expand All @@ -81,12 +89,12 @@ public static String removeJwtSign(String jwtToken) {
* @param exception original exception
* @return translated exception (better messaging and allow subsequent handling)
*/
public static RuntimeException handleJwtParserException(RuntimeException exception) {
if (exception instanceof ExpiredJwtException expiredJwtException) {
log.debug("Token with id '{}' for user '{}' is expired.", expiredJwtException.getClaims().getId(), expiredJwtException.getClaims().getSubject());
public RuntimeException handleJwtParserException(Exception exception) {
if (exception instanceof ExpiredJWTException) {
log.debug("Token is expired.");
return new TokenExpireException("Token is expired.", exception);
}
if (exception instanceof JwtException) {
if (exception instanceof BadJWTException || exception instanceof ParseException) {
log.debug(TOKEN_IS_NOT_VALID_DUE_TO, exception.getMessage());
return new TokenNotValidException("Token is not valid.", exception);
}
Expand All @@ -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
Expand All @@ -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<String> getFieldValuesFromToken(String token, List<String> pathToField) throws TokenFormatNotValidException {
public List<String> getFieldValuesFromToken(String token, List<String> 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<String> 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();
Expand All @@ -129,23 +141,24 @@ public static List<String> getFieldValuesFromToken(String token, List<String> pa
}
}

private List<String> extractHighLevelField(Claims claims, List<String> pathToField) {
return extractValueAsList(claims.get(pathToField.get(0)));
private List<String> extractHighLevelField(JWTClaimsSet claims, List<String> pathToField) {
return extractValueAsList(claims.getClaim(pathToField.get(0)));
}

@SuppressWarnings({ "unchecked", "rawtypes" })
private List<String> extractNestedFields(Claims claims, List<String> pathToField) {
@SuppressWarnings({ "rawtypes" })
private List<String> extractNestedFields(JWTClaimsSet claims, List<String> pathToField) {
var iterator = pathToField.iterator();
var key = iterator.next();
Map<String, Object> val = claims.get(key, Map.class);

var claim = claims.getClaim(key);
while (iterator.hasNext()) {
key = iterator.next();
if (iterator.hasNext()) {
val = (Map) val.get(key);
if (iterator.hasNext() && claim instanceof Map val) {
claim = val.get(key);
}
}

return extractValueAsList(val.get(key));
return extractValueAsList(((Map) claim).get(key));
}

@SuppressWarnings("unchecked")
Expand All @@ -157,6 +170,7 @@ private List<String> extractValueAsList(Object rawValue) {
} else {
throw new IllegalArgumentException("Field value is neither String nor List of Strings");
}

}

}
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,10 @@
* Copyright Contributors to the Zowe Project.
*/

package org.zowe.apiml.zaas.security.service;
package org.zowe.apiml.security.common.util;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.Header;
import io.jsonwebtoken.JwtException;
import com.nimbusds.jwt.proc.BadJWTException;
import com.nimbusds.jwt.proc.ExpiredJWTException;
import org.hamcrest.Matchers;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
Expand All @@ -29,35 +27,32 @@

import static org.hamcrest.MatcherAssert.assertThat;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.Mockito.mock;
import static org.zowe.apiml.zaas.utils.JWTUtils.createTokenWithUserFields;
import static org.zowe.apiml.security.common.util.JWTTestUtils.createTokenWithUserFields;

class JwtUtilsTest {

private static final String TOKEN_WITH_USERNAME_FIELDS = createTokenWithUserFields();

@Test
void testHandleJwtParserExceptionForExpiredToken() {

Exception exception = JwtUtils.handleJwtParserException(new ExpiredJwtException(mock(Header.class), mock(Claims.class), "msg"));
assertTrue(exception instanceof TokenExpireException);
Exception exception = JwtUtils.handleJwtParserException(new ExpiredJWTException("msg"));
assertInstanceOf(TokenExpireException.class, exception);
assertEquals("Token is expired.", exception.getMessage());
}

@Test
void testHandleJwtParserExceptionForInvalidToken() {

Exception exception = JwtUtils.handleJwtParserException(new JwtException("msg"));
assertTrue(exception instanceof TokenNotValidException);
Exception exception = JwtUtils.handleJwtParserException(new BadJWTException("msg"));
assertInstanceOf(TokenNotValidException.class, exception);
assertEquals("Token is not valid.", exception.getMessage());
}

@Test
void testHandleJwtParserRuntimeException() {
Exception exception = JwtUtils.handleJwtParserException(new RuntimeException("msg"));
assertTrue(exception instanceof TokenNotValidException);
assertInstanceOf(TokenNotValidException.class, exception);
assertEquals("An internal error occurred while validating the token therefore the token is no longer valid.", exception.getMessage());
}

Expand Down
Loading
Loading