Skip to content
3 changes: 2 additions & 1 deletion api-catalog-ui/frontend/cypress.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ module.exports = defineConfig({
'https://localhost:10010/gateway/oauth2/authorization/okta?returnUrl=https%3A%2F%2Flocalhost%3A10010%2Fapplication',
username: 'USER',
password: 'validPassword',
},
microservices: true,
},
viewportWidth: 1400,
viewportHeight: 980,
chromeWebSecurity: false,
Expand Down
13 changes: 9 additions & 4 deletions api-catalog-ui/frontend/cypress/e2e/graphql/graphql-apiml.cy.js
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,10 @@ describe('>>> GraphiQL Playground page test', () => {
cy.get('.graphiql-dialog-header h2').should('be.visible').should('contain', 'Settings');
});

// Skip flaky tests in the microservice setup
if (Cypress.env('microservices')) {
return;
}
it('Variable usage', () => {
login();
cy.contains('Discoverable client with GraphQL').click();
Expand All @@ -173,14 +177,15 @@ describe('>>> GraphiQL Playground page test', () => {

const variable = '{"id" :"book-1"}';

cy.get('.graphiql-editor').first()
cy.get('.graphiql-editor-tool').first()
.type(variable, {parseSpecialCharSequences: false});

cy.get('.graphiql-editor').then(($container) => {
cy.get('.graphiql-editor-tool').then(($container) => {
const text = $container.text().trim();
expect(text).to.include(variable);
});
});

it('Variable usage', () => {
login();
cy.contains('Discoverable client with GraphQL').click();
Expand All @@ -191,10 +196,10 @@ describe('>>> GraphiQL Playground page test', () => {

const header = '{"X-Custom-Header": "CustomValue"}';

cy.get('.graphiql-editor').first()
cy.get('.graphiql-editor-tool').first()
.type(header, {parseSpecialCharSequences: false});

cy.get('.graphiql-editor').then(($container) => {
cy.get('.graphiql-editor-tool').then(($container) => {
const text = $container.text().trim();
expect(text).to.include(header);
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
import org.zowe.apiml.message.core.MessageService;
import org.zowe.apiml.security.common.token.OIDCProvider;
import org.zowe.apiml.zaas.security.service.JwtSecurity;
import org.zowe.apiml.zaas.security.service.token.OIDCTokenProviderJWK;
import org.zowe.apiml.zaas.security.service.token.OIDCTokenProvider;
import org.zowe.apiml.zaas.security.service.zosmf.ZosmfService;
import reactor.core.publisher.Mono;

Expand Down Expand Up @@ -91,8 +91,8 @@ public Mono<Map<String, Object>> getAllPublicKeys() {
}
Optional<JWK> key = jwtSecurity.getJwkPublicKey();
key.ifPresent(keys::add);
if ((oidcProvider != null) && (oidcProvider instanceof OIDCTokenProviderJWK oidcTokenProviderJwk)) {
JWKSet oidcSet = oidcTokenProviderJwk.getJwkSet();
if ((oidcProvider != null) && (oidcProvider instanceof OIDCTokenProvider oidcTokenProvider)) {
JWKSet oidcSet = oidcTokenProvider.getJwkSet();
if (oidcSet != null) {
keys.addAll(oidcSet.getKeys());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
import org.zowe.apiml.message.core.MessageType;
import org.zowe.apiml.security.common.token.OIDCProvider;
import org.zowe.apiml.zaas.security.service.JwtSecurity;
import org.zowe.apiml.zaas.security.service.token.OIDCTokenProviderJWK;
import org.zowe.apiml.zaas.security.service.token.OIDCTokenProvider;
import org.zowe.apiml.zaas.security.service.zosmf.ZosmfService;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
Expand Down Expand Up @@ -69,7 +69,7 @@ void getAllPublicKeys_zosmfProducer_withOidc() throws Exception {
JWK oidcJwk = new RSAKey.Builder((RSAPublicKey) generateKeyPair().getPublic()).keyID("oidcKey").build();
JWKSet oidcKeySet = new JWKSet(oidcJwk);

OIDCTokenProviderJWK mockOidcProviderJwk = mock(OIDCTokenProviderJWK.class);
OIDCTokenProvider mockOidcProviderJwk = mock(OIDCTokenProvider.class);

when(jwtSecurity.actualJwtProducer()).thenReturn(JwtSecurity.JwtProducer.ZOSMF);
when(zosmfService.getPublicKeys()).thenReturn(zosmfKeySet);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@
import org.zowe.apiml.security.common.token.TokenNotValidException;
import org.zowe.apiml.zaas.security.service.AuthenticationService;
import org.zowe.apiml.zaas.security.service.JwtSecurity;
import org.zowe.apiml.zaas.security.service.token.OIDCTokenProviderJWK;
import org.zowe.apiml.zaas.security.service.token.OIDCTokenProvider;
import org.zowe.apiml.zaas.security.service.zosmf.ZosmfService;
import org.zowe.apiml.zaas.security.webfinger.WebFingerProvider;
import org.zowe.apiml.zaas.security.webfinger.WebFingerResponse;
Expand Down Expand Up @@ -354,8 +354,8 @@ public Map<String, Object> getAllPublicKeys() {
}
Optional<JWK> key = jwtSecurity.getJwkPublicKey();
key.ifPresent(keys::add);
if ((oidcProvider != null) && (oidcProvider instanceof OIDCTokenProviderJWK oidcTokenProviderJwk)) {
JWKSet oidcSet = oidcTokenProviderJwk.getJwkSet();
if ((oidcProvider != null) && (oidcProvider instanceof OIDCTokenProvider oidcTokenProvider)) {
JWKSet oidcSet = oidcTokenProvider.getJwkSet();
if (oidcSet != null) {
keys.addAll(oidcSet.getKeys());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,59 +14,50 @@
import com.nimbusds.jose.JOSEException;
import com.nimbusds.jose.jwk.JWK;
import com.nimbusds.jose.jwk.JWKSet;
import com.nimbusds.jose.jwk.KeyType;
import com.nimbusds.jose.jwk.KeyUse;
import com.nimbusds.jose.util.DefaultResourceRetriever;
import com.nimbusds.jose.util.Resource;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Clock;
import io.jsonwebtoken.JwtException;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.LocatorAdapter;
import io.jsonwebtoken.ProtectedHeader;
import io.jsonwebtoken.*;
import io.jsonwebtoken.lang.Collections;
import io.jsonwebtoken.security.UnsupportedKeyException;
import jakarta.annotation.PostConstruct;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
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.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.zowe.apiml.message.log.ApimlLogger;
import org.zowe.apiml.product.logging.annotations.InjectApimlLogger;
import org.zowe.apiml.constants.ApimlConstants;
import org.zowe.apiml.security.common.token.OIDCProvider;

import java.io.IOException;
import java.net.URL;
import java.security.Key;
import java.security.PublicKey;
import java.text.ParseException;
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;
import java.util.stream.Collectors;

@RequiredArgsConstructor
@Service
@Slf4j
@ConditionalOnExpression("'${apiml.security.oidc.validationType:JWK}' == 'JWK' && '${apiml.security.oidc.enabled:false}' == 'true'")
public class OIDCTokenProviderJWK implements OIDCProvider {
@ConditionalOnExpression("'${apiml.security.oidc.enabled:false}' == 'true'")
public class OIDCTokenProvider implements OIDCProvider {

private final LocatorAdapterKid keyLocator = new LocatorAdapterKid();

@InjectApimlLogger
protected final ApimlLogger logger = ApimlLogger.empty();

@Value("${apiml.security.oidc.registry:}")
String registry;

@Value("${apiml.security.oidc.jwks.uri}")
private String jwksUri;
private List<String> jwksUri;

@Value("${apiml.security.oidc.jwks.refreshInternalHours:1}")
private int jwkRefreshInterval;
Expand All @@ -75,11 +66,16 @@ public class OIDCTokenProviderJWK implements OIDCProvider {
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<String, PublicKey> publicKeys = new ConcurrentHashMap<>();
private final Map<String, JWK> publicKeys = new ConcurrentHashMap<>();
@Getter
private JWKSet jwkSet;


@PostConstruct
public void afterPropertiesSet() {
this.fetchJWKSet();
Expand All @@ -89,47 +85,60 @@ public void afterPropertiesSet() {

@Retryable
void fetchJWKSet() {
if (StringUtils.isBlank(jwksUri)) {
if (Collections.isEmpty(jwksUri)) {
log.debug("OIDC JWK URI not provided, JWK refresh not performed");
return;
}
log.debug("Refreshing JWK endpoints {}", jwksUri);

try {
publicKeys.clear();
jwkSet = null;
Resource resource = resourceRetriever.retrieveResource(new URL(jwksUri));
jwkSet = JWKSet.parse(resource.getContent());
publicKeys.putAll(processKeys(jwkSet));
} catch (IOException | ParseException | IllegalStateException e) {
log.error("Error processing response from URI {} message: {}", jwksUri, e.getMessage());
publicKeys.clear();
for (String url : jwksUri) {
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) {
log.error("Error processing response from URI {} message: {}", url, e.getMessage());
}
}
}
jwkSet = new JWKSet(publicKeys.values().stream().toList());

private Map<String, PublicKey> processKeys(JWKSet jwkKeys) {
return jwkKeys.getKeys().stream()
.filter(jwkKey -> {
KeyUse keyUse = jwkKey.getKeyUse();
KeyType keyType = jwkKey.getKeyType();
return keyUse != null && keyType != null && "sig".equals(keyUse.getValue()) && "RSA".equals(keyType.getValue());
})
.collect(Collectors.toMap(JWK::getKeyID, jwkKey -> {
try {
return jwkKey.toRSAKey().toRSAPublicKey();
} catch (JOSEException e) {
log.debug("Problem with getting RSA Public key from JWK. ", e.getCause());
throw new IllegalStateException("Failed to parse public key", e);
}
}));
}

@Override
public boolean isValid(String token) {
try {
log.debug("Validating the token with JWK: {}", jwksUri);
return !getClaims(token).isEmpty();
if (Collections.isEmpty(jwksUri) || getClaims(token).isEmpty()) {
return isValidExternal(token);
}
return true;
} catch (MalformedJwtException jwte) {
log.debug("Malformed JWT: {}", jwte.getMessage(), jwte.getCause());
return false;
} catch (JwtException jwte) {
log.debug("JWK token validation failed with the exception {}", jwte.getMessage(), jwte.getCause());
return isValidExternal(token);
}
}

public boolean isValidExternal(String token) {
try {
if (StringUtils.isBlank(endpointUrl)) {
log.debug("JWT can't be validated externally because endpoint URL was not provided.");
return false;
}
log.debug("Validating the token against URL: {}", endpointUrl);
var httpGet = new HttpGet(endpointUrl);
httpGet.addHeader(HttpHeaders.AUTHORIZATION, ApimlConstants.BEARER_AUTHENTICATION_PREFIX + " " + token);

return secureHttpClientWithKeystore.execute(httpGet, response -> {
final int responseCode = response.getCode();
log.debug("Response code: {}", responseCode);
return HttpStatus.valueOf(responseCode).is2xxSuccessful();
});
} catch (IOException e) {
log.error("An error occurred during validation of OIDC token using userInfo URI {}: {}", endpointUrl, e.getMessage());
return false;
}
}
Expand All @@ -155,10 +164,10 @@ class LocatorAdapterKid extends LocatorAdapter<Key> {

@Override
protected Key locate(ProtectedHeader header) {
if (jwkSet == null) {
if (jwkSet == null || jwkSet.isEmpty()) {
throw new JwtException("Could not validate the token due to missing public key.");
}
String kid = header.getKeyId();
var kid = header.getKeyId();
if (kid == null) {
throw new UnsupportedKeyException("Token does not provide kid. It uses an unsupported type of signature.");
}
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@
import org.zowe.apiml.util.HttpClientMockHelper;
import org.zowe.apiml.zaas.ZaasApplication;
import org.zowe.apiml.zaas.security.mapping.AuthenticationMapper;
import org.zowe.apiml.zaas.security.service.token.OIDCTokenProviderEndpoint;
import org.zowe.apiml.zaas.security.service.token.OIDCTokenProvider;

import javax.net.ssl.SSLContext;
import java.io.IOException;
Expand All @@ -62,14 +62,14 @@
},
classes = {
ZaasApplication.class,
OIDCTokenProviderEndpoint.class,
OIDCTokenProviderJWKEndpointTest.Config.class
OIDCTokenProvider.class,
OIDCTokenProviderEndpointTest.Config.class
},
webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT
)
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
@ActiveProfiles("OIDCTokenProviderEndpointTest")
class OIDCTokenProviderJWKEndpointTest {
class OIDCTokenProviderEndpointTest {

private static final String MF_USER = "USER";
private static final String VALID_TOKEN = "ewogICJ0eXAiOiAiSldUIiwKICAibm9uY2UiOiAiYVZhbHVlVG9CZVZlcmlmaWVkIiwKICAiYWxnIjogIlJTMjU2IiwKICAia2lkIjogIlNlQ1JldEtleSIKfQ.ewogICJhdWQiOiAiMDAwMDAwMDMtMDAwMC0wMDAwLWMwMDAtMDAwMDAwMDAwMDAwIiwKICAiaXNzIjogImh0dHBzOi8vb2lkYy5wcm92aWRlci5vcmcvYXBwIiwKICAiaWF0IjogMTcyMjUxNDEyOSwKICAibmJmIjogMTcyMjUxNDEyOSwKICAiZXhwIjogODcyMjUxODEyNSwKICAic3ViIjogIm9pZGMudXNlcm5hbWUiCn0.c29tZVNpZ25lZEhhc2hDb2Rl";
Expand Down
Loading
Loading