Skip to content

Commit 7dc6dad

Browse files
authored
Allow Zowe to run without a jwtsecret (#1203)
feat: Allow Zowe to run without a jwtsecret When jwtsecret is not required, API Mediation Layer can start without it.
1 parent 6fd599c commit 7dc6dad

File tree

23 files changed

+329
-214
lines changed

23 files changed

+329
-214
lines changed

common-service-core/src/main/java/org/zowe/apiml/security/SecurityUtils.java

Lines changed: 2 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,7 @@
1414
import org.zowe.apiml.message.log.ApimlLogger;
1515
import org.zowe.apiml.message.yaml.YamlMessageServiceInstance;
1616

17-
import java.io.File;
18-
import java.io.FileInputStream;
19-
import java.io.IOException;
20-
import java.io.InputStream;
17+
import java.io.*;
2118
import java.net.MalformedURLException;
2219
import java.net.URL;
2320
import java.security.*;
@@ -33,31 +30,6 @@ public class SecurityUtils {
3330

3431
public static final String SAFKEYRING = "safkeyring";
3532

36-
/**
37-
* Reads secret key from keystore or key ring, if keystore URL starts with {@value #SAFKEYRING}, and encode to Base64
38-
* @param config - {@link HttpsConfig} with mandatory filled fields: keyStore, keyStoreType, keyStorePassword, keyPassword,
39-
* and optional filled: keyAlias and trustStore
40-
* @return Base64 encoded secret key in {@link String}
41-
*/
42-
public static String readSecret(HttpsConfig config) {
43-
if (config.getKeyStore() != null) {
44-
try {
45-
Key key = loadKey(config);
46-
if (key == null) {
47-
throw new UnrecoverableKeyException(String.format(
48-
"No key with private key entry could be used in the keystore. Provided key alias: %s",
49-
config.getKeyAlias() == null ? "<not provided>" : config.getKeyAlias()));
50-
}
51-
return Base64.getEncoder().encodeToString(key.getEncoded());
52-
} catch (UnrecoverableKeyException e) {
53-
apimlLog.log("org.zowe.apiml.common.errorReadingSecretKey", e.getMessage());
54-
throw new HttpsConfigError("Error reading secret key: " + e.getMessage(), e,
55-
HttpsConfigError.ErrorCode.HTTP_CLIENT_INITIALIZATION_FAILED, config);
56-
}
57-
}
58-
return null;
59-
}
60-
6133
/**
6234
* Loads secret key from keystore or key ring, if keystore URL starts with {@value #SAFKEYRING}
6335
* @param config - {@link HttpsConfig} with mandatory filled fields: keyStore, keyStoreType, keyStorePassword, keyPassword,
@@ -73,7 +45,7 @@ public static Key loadKey(HttpsConfig config) {
7345
if (config.getKeyAlias() != null) {
7446
key = ks.getKey(config.getKeyAlias(), keyPasswordInChars);
7547
} else {
76-
key = findFirstSecretKey(ks, keyPasswordInChars);
48+
throw new KeyStoreException("No key alias provided.");
7749
}
7850
return key;
7951
} catch (NoSuchAlgorithmException | KeyStoreException | CertificateException | IOException
@@ -124,22 +96,6 @@ public static Set<String> loadCertificateChainBase64(HttpsConfig config) throws
12496
return out;
12597
}
12698

127-
private static Key findFirstSecretKey(KeyStore keyStore, char[] keyPasswordInChars) throws KeyStoreException, NoSuchAlgorithmException {
128-
Key key = null;
129-
for (Enumeration<String> e = keyStore.aliases(); e.hasMoreElements(); ) {
130-
String alias = e.nextElement();
131-
try {
132-
key = keyStore.getKey(alias, keyPasswordInChars);
133-
if (key != null) {
134-
break;
135-
}
136-
} catch (UnrecoverableKeyException uke) {
137-
log.debug("Key with alias {} could not be used: {}", alias, uke.getMessage());
138-
}
139-
}
140-
return key;
141-
}
142-
14399
/**
144100
* Loads public key from keystore or key ring, if keystore URL starts with {@value #SAFKEYRING}
145101
* @param config - {@link HttpsConfig} with mandatory filled fields: keyStore, keyStoreType, keyStorePassword, keyPassword,

common-service-core/src/test/java/org/zowe/apiml/security/SecurityUtilsTest.java

Lines changed: 0 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -43,20 +43,6 @@ void setUp() {
4343
httpsConfigBuilder = SecurityTestUtils.correctHttpsSettings();
4444
}
4545

46-
@Test
47-
void testReadSecret() {
48-
HttpsConfig httpsConfig = httpsConfigBuilder.keyAlias(KEY_ALIAS).build();
49-
String secretKey = SecurityUtils.readSecret(httpsConfig);
50-
assertNotNull(secretKey);
51-
}
52-
53-
@Test
54-
void testReadSecretWithIncorrectKeyAlias() {
55-
HttpsConfig httpsConfig = httpsConfigBuilder.keyAlias(WRONG_PARAMETER).build();
56-
assertThrows(HttpsConfigError.class, () -> SecurityUtils.readSecret(httpsConfig));
57-
58-
}
59-
6046
@Test
6147
void testLoadKey() {
6248
HttpsConfig httpsConfig = httpsConfigBuilder.keyAlias(JWT_KEY_ALIAS).build();

config/local/gateway-service.yml

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,6 @@ apiml:
1313
timeout: 360 # [s] - default timeout to expire (z/OS has 10 mins as default)
1414
ssl:
1515
verifySslCertificatesOfServices: true
16-
zosmf:
17-
useJwtToken: true # if true and z/OSMF returns JWT token use it, otherwise create Zowe JWT token with LTPA token from z/OSMF, default is true
1816
x509:
1917
enabled: true
2018
banner: console

gateway-service/build.gradle

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,8 @@ dependencies {
7070
implementation libraries.nimbusJoseJwt
7171
implementation libraries.eh_cache
7272
implementation libraries.spring_retry
73+
implementation libraries.awaitility
74+
7375
compileOnly libraries.javax_inject
7476
compileOnly libraries.lombok
7577
annotationProcessor libraries.lombok

gateway-service/src/main/java/org/zowe/apiml/gateway/controllers/AuthController.java

Lines changed: 7 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,7 @@
1212
import com.nimbusds.jose.jwk.JWK;
1313
import com.nimbusds.jose.jwk.JWKSet;
1414
import lombok.RequiredArgsConstructor;
15-
import lombok.Setter;
1615
import net.minidev.json.JSONObject;
17-
import org.springframework.beans.factory.annotation.Value;
1816
import org.springframework.web.bind.annotation.*;
1917
import org.zowe.apiml.gateway.security.service.AuthenticationService;
2018
import org.zowe.apiml.gateway.security.service.JwtSecurityInitializer;
@@ -25,6 +23,7 @@
2523
import javax.servlet.http.HttpServletResponse;
2624
import java.util.LinkedList;
2725
import java.util.List;
26+
import java.util.Optional;
2827

2928
import static org.apache.http.HttpStatus.*;
3029

@@ -37,10 +36,6 @@
3736
@RequestMapping(AuthController.CONTROLLER_PATH)
3837
public class AuthController {
3938

40-
@Setter
41-
@Value("${apiml.security.zosmf.useJwtToken:true}")
42-
protected boolean useZosmfJwtToken;
43-
4439
private final AuthenticationService authenticationService;
4540

4641
private final JwtSecurityInitializer jwtSecurityInitializer;
@@ -87,19 +82,19 @@ public void distributeInvalidate(HttpServletRequest request, HttpServletResponse
8782
public JSONObject getAllPublicKeys() {
8883
final List<JWK> keys = new LinkedList<>();
8984
keys.addAll(zosmfService.getPublicKeys().getKeys());
90-
keys.add(jwtSecurityInitializer.getJwkPublicKey());
85+
Optional<JWK> key = jwtSecurityInitializer.getJwkPublicKey();
86+
key.ifPresent(keys::add);
9187
return new JWKSet(keys).toJSONObject(true);
9288
}
9389

9490
@GetMapping(path = CURRENT_PUBLIC_KEYS_PATH)
9591
@ResponseBody
9692
public JSONObject getCurrentPublicKeys() {
97-
final List<JWK> keys = new LinkedList<>();
98-
if (useZosmfJwtToken) {
99-
keys.addAll(zosmfService.getPublicKeys().getKeys());
100-
}
93+
final List<JWK> keys = new LinkedList<>(zosmfService.getPublicKeys().getKeys());
94+
10195
if (keys.isEmpty()) {
102-
keys.add(jwtSecurityInitializer.getJwkPublicKey());
96+
Optional<JWK> key = jwtSecurityInitializer.getJwkPublicKey();
97+
key.ifPresent(keys::add);
10398
}
10499
return new JWKSet(keys).toJSONObject(true);
105100
}

gateway-service/src/main/java/org/zowe/apiml/gateway/health/GatewayHealthIndicator.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ protected void doHealthCheck(Health.Builder builder) {
5454
boolean authUp = true;
5555
if (loginProviders.isZosfmUsed()) {
5656
try {
57-
authUp = loginProviders.isZosmfAvailable();
57+
authUp = loginProviders.isZosmfAvailableAndOnline();
5858
} catch (AuthenticationServiceException ex) {
5959
System.exit(-1);
6060
}

gateway-service/src/main/java/org/zowe/apiml/gateway/security/config/ComponentsConfiguration.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
import org.springframework.context.annotation.*;
1414
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
1515
import org.zowe.apiml.gateway.security.login.Providers;
16+
import org.zowe.apiml.gateway.security.service.zosmf.ZosmfService;
1617
import org.zowe.apiml.passticket.PassTicketService;
1718
import org.zowe.apiml.security.common.config.AuthConfigurationProperties;
1819

@@ -48,9 +49,10 @@ public PassTicketService passTicketService() {
4849
public Providers loginProviders(
4950
DiscoveryClient discoveryClient,
5051
AuthConfigurationProperties authConfigurationProperties,
52+
ZosmfService zosmfService,
5153
@Lazy CompoundAuthProvider compoundAuthProvider
5254
) {
53-
return new Providers(discoveryClient, authConfigurationProperties, compoundAuthProvider);
55+
return new Providers(discoveryClient, authConfigurationProperties, compoundAuthProvider, zosmfService);
5456
}
5557

5658
}

gateway-service/src/main/java/org/zowe/apiml/gateway/security/login/Providers.java

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,24 +10,49 @@
1010
package org.zowe.apiml.gateway.security.login;
1111

1212
import lombok.RequiredArgsConstructor;
13+
import lombok.extern.slf4j.Slf4j;
1314
import org.springframework.cloud.client.discovery.DiscoveryClient;
1415
import org.springframework.security.authentication.AuthenticationServiceException;
1516
import org.zowe.apiml.gateway.security.config.CompoundAuthProvider;
17+
import org.zowe.apiml.gateway.security.service.zosmf.ZosmfService;
1618
import org.zowe.apiml.security.common.config.AuthConfigurationProperties;
19+
import org.zowe.apiml.security.common.error.ServiceNotAccessibleException;
1720

21+
@Slf4j
1822
@RequiredArgsConstructor
1923
public class Providers {
2024
private final DiscoveryClient discoveryClient;
2125
private final AuthConfigurationProperties authConfigurationProperties;
2226
private final CompoundAuthProvider compoundAuthProvider;
27+
private final ZosmfService zosmfService;
2328

2429
/**
2530
* This method decides whether the Zosmf service is available.
2631
* @return Availability of the ZOSMF service in the system.
2732
* @throws AuthenticationServiceException if the z/OSMF service id is not configured
2833
*/
2934
public boolean isZosmfAvailable() {
30-
return !this.discoveryClient.getInstances(authConfigurationProperties.validatedZosmfServiceId()).isEmpty();
35+
boolean isZosmfRegisteredAndPropagated = !this.discoveryClient.getInstances(authConfigurationProperties.validatedZosmfServiceId()).isEmpty();
36+
log.debug("zOSMF registered with the Discovery Service and propagated to Gateway: {}", isZosmfRegisteredAndPropagated);
37+
return isZosmfRegisteredAndPropagated;
38+
}
39+
40+
/**
41+
* Verify that the zOSMF is registered in the Discovery service and that we can actually reach it.
42+
* @return true if the service is registered and properly responds.
43+
*/
44+
public boolean isZosmfAvailableAndOnline() {
45+
try {
46+
boolean isAvailable = isZosmfAvailable();
47+
boolean isAccessible = zosmfService.isAccessible();
48+
log.debug("zOSMF is registered and propagated to the DS: {} and is accessible based on the information: {}", isAvailable, isAccessible);
49+
50+
return isAvailable && isAccessible;
51+
} catch (ServiceNotAccessibleException exception) {
52+
log.debug("zOSMF isn't registered to the Gateway yet");
53+
54+
return false;
55+
}
3156
}
3257

3358
/**
@@ -37,4 +62,12 @@ public boolean isZosmfAvailable() {
3762
public boolean isZosfmUsed() {
3863
return compoundAuthProvider.getLoginAuthProviderName().equalsIgnoreCase(LoginProvider.ZOSMF.toString());
3964
}
65+
66+
/**
67+
* This method decides whether used zOSMF instance supports JWT tokens.
68+
* @return True is the instance support JWT
69+
*/
70+
public boolean zosmfSupportsJwt() {
71+
return zosmfService.loginEndpointExists();
72+
}
4073
}

gateway-service/src/main/java/org/zowe/apiml/gateway/security/login/zosmf/ZosmfAuthenticationProvider.java

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@
1010
package org.zowe.apiml.gateway.security.login.zosmf;
1111

1212
import lombok.RequiredArgsConstructor;
13-
import org.springframework.beans.factory.annotation.Value;
1413
import org.springframework.security.authentication.AuthenticationProvider;
1514
import org.springframework.security.authentication.BadCredentialsException;
1615
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
@@ -29,9 +28,6 @@
2928
@RequiredArgsConstructor
3029
public class ZosmfAuthenticationProvider implements AuthenticationProvider {
3130

32-
@Value("${apiml.security.zosmf.useJwtToken:true}")
33-
private boolean useJwtToken;
34-
3531
private final AuthenticationService authenticationService;
3632
private final ZosmfService zosmfService;
3733

@@ -48,7 +44,7 @@ public Authentication authenticate(Authentication authentication) {
4844
final ZosmfService.AuthenticationResponse ar = zosmfService.authenticate(authentication);
4945

5046
// if z/OSMF support JWT, use it as Zowe JWT token
51-
if (ar.getTokens().containsKey(JWT) && useJwtToken) {
47+
if (ar.getTokens().containsKey(JWT)) {
5248
return authenticationService.createTokenAuthentication(user, ar.getTokens().get(JWT));
5349
}
5450

0 commit comments

Comments
 (0)