From 762593a265865b8bcb98f65d6d7ed2b7bdf31d14 Mon Sep 17 00:00:00 2001 From: Sandor Molnar Date: Tue, 28 Apr 2026 11:50:20 +0200 Subject: [PATCH 01/14] KNOX-3310: Fix redundant ALIAS_PASSPHRASE assignment and improve logging (#1216) --- .../src/main/resources/docker/gateway-entrypoint.sh | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/gateway-docker/src/main/resources/docker/gateway-entrypoint.sh b/gateway-docker/src/main/resources/docker/gateway-entrypoint.sh index 015dbe2fac..2cb0277a3d 100755 --- a/gateway-docker/src/main/resources/docker/gateway-entrypoint.sh +++ b/gateway-docker/src/main/resources/docker/gateway-entrypoint.sh @@ -42,10 +42,10 @@ set -o pipefail ## Helper function used to import certs into truststore ## Function takes cert file as argument +## At this time ALIAS_PASSPHRASE is already initialized importMultipleCerts() { FILE=$1 local import_failed=0 - ALIAS_PASSPHRASE=$(/bin/cat "${KEYSTORE_PASSWORD_FILE}") # number of certs in the PEM file CERTS=$(/bin/grep 'END CERTIFICATE' "$FILE"| /usr/bin/wc -l) # For every cert in the PEM file, extract it and import into the JKS keystore @@ -139,10 +139,11 @@ fi if [[ -n ${KEYSTORE_PASSWORD_FILE} ]] && [[ -f ${KEYSTORE_PASSWORD_FILE} ]] then - echo "Using provided keystore password file" + echo "Setting ALIAS_PASSPHRASE from provided keystore password file: ${KEYSTORE_PASSWORD_FILE}" ALIAS_PASSPHRASE=$(/bin/cat "${KEYSTORE_PASSWORD_FILE}" 2> /dev/null) else # If keystore password is not provided use master secret as alias passphrase + echo "Setting ALIAS_PASSPHRASE to MASTER_SECRET" ALIAS_PASSPHRASE="${MASTER_SECRET}" fi From 4f94d957c8f2da9a2a8e3efedfa1ca9fc2b482bc Mon Sep 17 00:00:00 2001 From: hanicz Date: Wed, 29 Apr 2026 13:00:06 +0200 Subject: [PATCH 02/14] =?UTF-8?q?KNOX-3311:=20Fix=20X509CertificateUtil.fe?= =?UTF-8?q?tchPublicCertsFromServer=20issue=20w=E2=80=A6=20(#1217)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * KNOX-3311: Fix X509CertificateUtil.fetchPublicCertsFromServer issue where default truststore can't be loaded due to invalid/missing password * KNOX-3311: Added unit tests --- gateway-discovery-cm/pom.xml | 4 + .../discovery/cm/ApiClientFactory.java | 16 ++-- .../discovery/cm/ApiClientFactoryTest.java | 11 ++- .../PollingConfigurationAnalyzerTest.java | 6 +- .../metadata/KnoxMetadataResource.java | 10 +- .../org/apache/knox/gateway/shell/KnoxSh.java | 3 +- .../util/TruststorePasswordSetter.java | 37 ++++++++ .../gateway/util/X509CertificateUtil.java | 13 ++- .../util/TruststorePasswordSetterTest.java | 71 ++++++++++++++ .../gateway/util/X509CertificateUtilTest.java | 94 +++++++++++++++++++ 10 files changed, 240 insertions(+), 25 deletions(-) create mode 100644 gateway-util-common/src/main/java/org/apache/knox/gateway/util/TruststorePasswordSetter.java create mode 100644 gateway-util-common/src/test/java/org/apache/knox/gateway/util/TruststorePasswordSetterTest.java create mode 100644 gateway-util-common/src/test/java/org/apache/knox/gateway/util/X509CertificateUtilTest.java diff --git a/gateway-discovery-cm/pom.xml b/gateway-discovery-cm/pom.xml index 8a484cb8ce..86332b9d5d 100644 --- a/gateway-discovery-cm/pom.xml +++ b/gateway-discovery-cm/pom.xml @@ -63,6 +63,10 @@ org.apache.knox gateway-util-configinjector + + org.apache.knox + gateway-util-common + com.cloudera.api.swagger cloudera-manager-api-swagger diff --git a/gateway-discovery-cm/src/main/java/org/apache/knox/gateway/topology/discovery/cm/ApiClientFactory.java b/gateway-discovery-cm/src/main/java/org/apache/knox/gateway/topology/discovery/cm/ApiClientFactory.java index 6ba9f90068..ce1babeb3d 100644 --- a/gateway-discovery-cm/src/main/java/org/apache/knox/gateway/topology/discovery/cm/ApiClientFactory.java +++ b/gateway-discovery-cm/src/main/java/org/apache/knox/gateway/topology/discovery/cm/ApiClientFactory.java @@ -21,28 +21,26 @@ import org.apache.knox.gateway.services.security.AliasService; import org.apache.knox.gateway.services.security.AliasServiceException; import org.apache.knox.gateway.topology.discovery.ServiceDiscoveryConfig; +import org.apache.knox.gateway.util.TruststorePasswordSetter; import java.security.KeyStore; public class ApiClientFactory { private static final ClouderaManagerServiceDiscoveryMessages LOG = MessagesFactory.get(ClouderaManagerServiceDiscoveryMessages.class); - static final String TRUSTSTORE_PASSWORD_SYSTEM_PROPERTY = "javax.net.ssl.trustStorePassword"; - public static final String TRUSTSTORE_PASSWORD_ALIAS = "cm.discovery.trustStorePassword"; public static DiscoveryApiClient getApiClient(final GatewayConfig gatewayConfig, final ServiceDiscoveryConfig discoveryConfig, final AliasService aliasService, final KeyStore truststore) { + final char[] trustStorePassword; try { - final char[] trustStorePassword = aliasService.getPasswordFromAliasForGateway(TRUSTSTORE_PASSWORD_ALIAS); - if (trustStorePassword != null && trustStorePassword.length > 0) { - System.setProperty(TRUSTSTORE_PASSWORD_SYSTEM_PROPERTY, new String(trustStorePassword)); - } - return new DiscoveryApiClient(gatewayConfig, discoveryConfig, aliasService, truststore); + trustStorePassword = aliasService.getPasswordFromAliasForGateway(TruststorePasswordSetter.TRUSTSTORE_PASSWORD_ALIAS); } catch (AliasServiceException e) { LOG.clouderaManagerApiClientBuildError(e); throw new ServiceDiscoveryException("Unable to retrieve CM service discovery truststore password", e); - } finally { - System.clearProperty(TRUSTSTORE_PASSWORD_SYSTEM_PROPERTY); + } + + try (TruststorePasswordSetter ignored = new TruststorePasswordSetter(trustStorePassword)) { + return new DiscoveryApiClient(gatewayConfig, discoveryConfig, aliasService, truststore); } } } diff --git a/gateway-discovery-cm/src/test/java/org/apache/knox/gateway/topology/discovery/cm/ApiClientFactoryTest.java b/gateway-discovery-cm/src/test/java/org/apache/knox/gateway/topology/discovery/cm/ApiClientFactoryTest.java index 322166fda2..6125f47c5b 100644 --- a/gateway-discovery-cm/src/test/java/org/apache/knox/gateway/topology/discovery/cm/ApiClientFactoryTest.java +++ b/gateway-discovery-cm/src/test/java/org/apache/knox/gateway/topology/discovery/cm/ApiClientFactoryTest.java @@ -21,6 +21,7 @@ import org.apache.knox.gateway.services.security.AliasService; import org.apache.knox.gateway.services.security.AliasServiceException; import org.apache.knox.gateway.topology.discovery.ServiceDiscoveryConfig; +import org.apache.knox.gateway.util.TruststorePasswordSetter; import org.easymock.EasyMock; import org.junit.After; import org.junit.Assert; @@ -77,9 +78,9 @@ private void testGetApiClient(final boolean shouldSetSystemProperty, String trus EasyMock.expect(serviceDiscoveryConfig.getPasswordAlias()).andReturn("myCmPasswordAlias").anyTimes(); final AliasService aliasService = EasyMock.createMock(AliasService.class); if (shouldSetSystemProperty) { - EasyMock.expect(aliasService.getPasswordFromAliasForGateway(ApiClientFactory.TRUSTSTORE_PASSWORD_ALIAS)).andReturn(trustStorePassword.toCharArray()).anyTimes(); + EasyMock.expect(aliasService.getPasswordFromAliasForGateway(TruststorePasswordSetter.TRUSTSTORE_PASSWORD_ALIAS)).andReturn(trustStorePassword.toCharArray()).anyTimes(); } else { - EasyMock.expect(aliasService.getPasswordFromAliasForGateway(ApiClientFactory.TRUSTSTORE_PASSWORD_ALIAS)).andReturn(null).anyTimes(); + EasyMock.expect(aliasService.getPasswordFromAliasForGateway(TruststorePasswordSetter.TRUSTSTORE_PASSWORD_ALIAS)).andReturn(null).anyTimes(); } EasyMock.expect(aliasService.getPasswordFromAliasForGateway("myCmPasswordAlias")).andReturn("myCmPassword".toCharArray()).anyTimes(); final KeyStore trustStore = EasyMock.createMock(KeyStore.class); @@ -88,13 +89,13 @@ private void testGetApiClient(final boolean shouldSetSystemProperty, String trus ApiClientFactory.getApiClient(gatewayConfig, serviceDiscoveryConfig, aliasService, trustStore); if (shouldSetSystemProperty && StringUtils.isNotBlank(trustStorePassword)) { - Assert.assertEquals(ApiClientFactory.TRUSTSTORE_PASSWORD_SYSTEM_PROPERTY, testProps.lastSetKey); + Assert.assertEquals(TruststorePasswordSetter.TRUSTSTORE_PASSWORD_SYSTEM_PROPERTY, testProps.lastSetKey); Assert.assertEquals(trustStorePassword, testProps.lastSetValue); - Assert.assertEquals(ApiClientFactory.TRUSTSTORE_PASSWORD_SYSTEM_PROPERTY, testProps.lastRemovedKey); + Assert.assertEquals(TruststorePasswordSetter.TRUSTSTORE_PASSWORD_SYSTEM_PROPERTY, testProps.lastRemovedKey); } else { Assert.assertNull(testProps.lastSetKey); Assert.assertNull(testProps.lastSetValue); - Assert.assertEquals(ApiClientFactory.TRUSTSTORE_PASSWORD_SYSTEM_PROPERTY, testProps.lastRemovedKey); + Assert.assertEquals(TruststorePasswordSetter.TRUSTSTORE_PASSWORD_SYSTEM_PROPERTY, testProps.lastRemovedKey); } } diff --git a/gateway-discovery-cm/src/test/java/org/apache/knox/gateway/topology/discovery/cm/monitor/PollingConfigurationAnalyzerTest.java b/gateway-discovery-cm/src/test/java/org/apache/knox/gateway/topology/discovery/cm/monitor/PollingConfigurationAnalyzerTest.java index 5d734027b9..9d6dbd95bd 100644 --- a/gateway-discovery-cm/src/test/java/org/apache/knox/gateway/topology/discovery/cm/monitor/PollingConfigurationAnalyzerTest.java +++ b/gateway-discovery-cm/src/test/java/org/apache/knox/gateway/topology/discovery/cm/monitor/PollingConfigurationAnalyzerTest.java @@ -32,9 +32,9 @@ import org.apache.knox.gateway.services.topology.impl.GatewayStatusService; import org.apache.knox.gateway.topology.ClusterConfigurationMonitorService; import org.apache.knox.gateway.topology.discovery.ServiceDiscoveryConfig; -import org.apache.knox.gateway.topology.discovery.cm.ApiClientFactory; import org.apache.knox.gateway.topology.discovery.cm.model.hdfs.NameNodeServiceModelGenerator; import org.apache.knox.gateway.topology.discovery.cm.model.hive.HiveOnTezServiceModelGenerator; +import org.apache.knox.gateway.util.TruststorePasswordSetter; import org.easymock.EasyMock; import org.junit.After; import org.junit.Test; @@ -363,7 +363,7 @@ public void testClusterConfigMonitorTerminationForNoLongerReferencedClusters() t EasyMock.replay(ts, ccms, gatewayStatusService, gws); AliasService aliasService = EasyMock.createNiceMock(AliasService.class); - EasyMock.expect(aliasService.getPasswordFromAliasForGateway(ApiClientFactory.TRUSTSTORE_PASSWORD_ALIAS)).andReturn(null).anyTimes(); + EasyMock.expect(aliasService.getPasswordFromAliasForGateway(TruststorePasswordSetter.TRUSTSTORE_PASSWORD_ALIAS)).andReturn(null).anyTimes(); EasyMock.replay(aliasService); try { @@ -540,7 +540,7 @@ private TestablePollingConfigAnalyzer buildPollingConfigAnalyzer(final String ad EasyMock.replay(configCache); AliasService aliasService = EasyMock.createNiceMock(AliasService.class); - EasyMock.expect(aliasService.getPasswordFromAliasForGateway(ApiClientFactory.TRUSTSTORE_PASSWORD_ALIAS)).andReturn(null).anyTimes(); + EasyMock.expect(aliasService.getPasswordFromAliasForGateway(TruststorePasswordSetter.TRUSTSTORE_PASSWORD_ALIAS)).andReturn(null).anyTimes(); EasyMock.replay(aliasService); if (isKnoxGatewayReady) { diff --git a/gateway-service-metadata/src/main/java/org/apache/knox/gateway/service/metadata/KnoxMetadataResource.java b/gateway-service-metadata/src/main/java/org/apache/knox/gateway/service/metadata/KnoxMetadataResource.java index a01d5a554c..75eb88baa4 100644 --- a/gateway-service-metadata/src/main/java/org/apache/knox/gateway/service/metadata/KnoxMetadataResource.java +++ b/gateway-service-metadata/src/main/java/org/apache/knox/gateway/service/metadata/KnoxMetadataResource.java @@ -69,6 +69,7 @@ import org.apache.knox.gateway.topology.Service; import org.apache.knox.gateway.topology.Topology; import org.apache.knox.gateway.util.JsonUtils; +import org.apache.knox.gateway.util.TruststorePasswordSetter; import org.apache.knox.gateway.util.X509CertificateUtil; import com.kstruct.gethostname4j.Hostname; @@ -173,11 +174,16 @@ private Response generateFailureFileDownloadResponse(Status status, String error private Certificate[] getPublicCertificates() { try { - return X509CertificateUtil.fetchPublicCertsFromServer(request.getRequestURL().toString(), true, null); + final GatewayServices gatewayServices = (GatewayServices) request.getServletContext().getAttribute(GatewayServices.GATEWAY_SERVICES_ATTRIBUTE); + if (gatewayServices != null) { + final AliasService aliasService = gatewayServices.getService(ServiceType.ALIAS_SERVICE); + char[] trustStorePassword = aliasService.getPasswordFromAliasForGateway(TruststorePasswordSetter.TRUSTSTORE_PASSWORD_ALIAS); + return X509CertificateUtil.fetchPublicCertsFromServer(request.getRequestURL().toString(), trustStorePassword, true, null); + } } catch (Exception e) { LOG.failedToFetchPublicCert(e.getMessage(), e); - return null; } + return null; } private void generateCertificatePem(Certificate[] certificateChain, GatewayConfig gatewayConfig) { diff --git a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/KnoxSh.java b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/KnoxSh.java index ec169b51d7..a9bce5f9db 100644 --- a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/KnoxSh.java +++ b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/KnoxSh.java @@ -181,7 +181,8 @@ class KnoxBuildTrustStore extends Command { public void execute() throws Exception { String result = GATEWAY_CERT_NOT_EXPORTED; try { - final X509Certificate[] gatewayServerPublicCerts = X509CertificateUtil.fetchPublicCertsFromServer(gateway, false, out); + final X509Certificate[] gatewayServerPublicCerts = X509CertificateUtil.fetchPublicCertsFromServer( + gateway, ClientTrustStoreHelper.getClientTrustStoreFilePassword().toCharArray(), false, out); if (gatewayServerPublicCerts != null) { final File trustStoreFile = ClientTrustStoreHelper.getClientTrustStoreFile(); final String trustStorePassword = ClientTrustStoreHelper.getClientTrustStoreFilePassword(); diff --git a/gateway-util-common/src/main/java/org/apache/knox/gateway/util/TruststorePasswordSetter.java b/gateway-util-common/src/main/java/org/apache/knox/gateway/util/TruststorePasswordSetter.java new file mode 100644 index 0000000000..9de37e6179 --- /dev/null +++ b/gateway-util-common/src/main/java/org/apache/knox/gateway/util/TruststorePasswordSetter.java @@ -0,0 +1,37 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.knox.gateway.util; + +/** + * A utility class that sets and clears the javax.net.ssl.trustStorePassword system property. + */ +public class TruststorePasswordSetter implements AutoCloseable { + public static final String TRUSTSTORE_PASSWORD_SYSTEM_PROPERTY = "javax.net.ssl.trustStorePassword"; + public static final String TRUSTSTORE_PASSWORD_ALIAS = "cm.discovery.trustStorePassword"; + + public TruststorePasswordSetter(char[] password) { + if (password != null && password.length > 0) { + System.setProperty(TRUSTSTORE_PASSWORD_SYSTEM_PROPERTY, new String(password)); + } + } + + @Override + public void close() { + System.clearProperty(TRUSTSTORE_PASSWORD_SYSTEM_PROPERTY); + } +} diff --git a/gateway-util-common/src/main/java/org/apache/knox/gateway/util/X509CertificateUtil.java b/gateway-util-common/src/main/java/org/apache/knox/gateway/util/X509CertificateUtil.java index 93cfb0d3ae..8d0574d209 100644 --- a/gateway-util-common/src/main/java/org/apache/knox/gateway/util/X509CertificateUtil.java +++ b/gateway-util-common/src/main/java/org/apache/knox/gateway/util/X509CertificateUtil.java @@ -232,11 +232,14 @@ public static boolean isSelfSignedCertificate(Certificate certificate) { } } - public static X509Certificate[] fetchPublicCertsFromServer(String serverUrl, boolean forceReturnCert, PrintStream out) throws Exception { - final TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); - trustManagerFactory.init((KeyStore) null); - final X509TrustManager defaultTrustManager = (X509TrustManager) trustManagerFactory.getTrustManagers()[0]; - final CertificateChainAwareTrustManager trustManagerWithCertificateChain = new CertificateChainAwareTrustManager(defaultTrustManager); + public static X509Certificate[] fetchPublicCertsFromServer(String serverUrl, char[] trustStorePassword, boolean forceReturnCert, PrintStream out) throws Exception { + CertificateChainAwareTrustManager trustManagerWithCertificateChain; + try (TruststorePasswordSetter ignored = new TruststorePasswordSetter(trustStorePassword)) { + final TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); + trustManagerFactory.init((KeyStore) null); + final X509TrustManager defaultTrustManager = (X509TrustManager) trustManagerFactory.getTrustManagers()[0]; + trustManagerWithCertificateChain = new CertificateChainAwareTrustManager(defaultTrustManager); + } final SSLContext sslContext = SSLContext.getInstance("TLS"); sslContext.init(null, new TrustManager[] { trustManagerWithCertificateChain }, null); diff --git a/gateway-util-common/src/test/java/org/apache/knox/gateway/util/TruststorePasswordSetterTest.java b/gateway-util-common/src/test/java/org/apache/knox/gateway/util/TruststorePasswordSetterTest.java new file mode 100644 index 0000000000..3a43b6ec93 --- /dev/null +++ b/gateway-util-common/src/test/java/org/apache/knox/gateway/util/TruststorePasswordSetterTest.java @@ -0,0 +1,71 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.knox.gateway.util; + +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +public class TruststorePasswordSetterTest { + private String originalPassword; + + @Before + public void setUp() { + originalPassword = System.getProperty(TruststorePasswordSetter.TRUSTSTORE_PASSWORD_SYSTEM_PROPERTY); + System.clearProperty(TruststorePasswordSetter.TRUSTSTORE_PASSWORD_SYSTEM_PROPERTY); + } + + @After + public void tearDown() { + if (originalPassword != null) { + System.setProperty(TruststorePasswordSetter.TRUSTSTORE_PASSWORD_SYSTEM_PROPERTY, originalPassword); + } else { + System.clearProperty(TruststorePasswordSetter.TRUSTSTORE_PASSWORD_SYSTEM_PROPERTY); + } + } + + @Test + public void testSetPassword() { + final String password = "test-password"; + try (TruststorePasswordSetter setter = new TruststorePasswordSetter(password.toCharArray())) { + Assert.assertEquals("The system property should be set to the provided password.", + password, System.getProperty(TruststorePasswordSetter.TRUSTSTORE_PASSWORD_SYSTEM_PROPERTY)); + } + Assert.assertNull("The system property should be cleared after closing the setter.", + System.getProperty(TruststorePasswordSetter.TRUSTSTORE_PASSWORD_SYSTEM_PROPERTY)); + } + + @Test + public void testNullPassword() { + try (TruststorePasswordSetter setter = new TruststorePasswordSetter(null)) { + Assert.assertNull("The system property should not be set if the password is null.", + System.getProperty(TruststorePasswordSetter.TRUSTSTORE_PASSWORD_SYSTEM_PROPERTY)); + } + Assert.assertNull(System.getProperty(TruststorePasswordSetter.TRUSTSTORE_PASSWORD_SYSTEM_PROPERTY)); + } + + @Test + public void testEmptyPassword() { + try (TruststorePasswordSetter setter = new TruststorePasswordSetter(new char[0])) { + Assert.assertNull("The system property should not be set if the password array is empty.", + System.getProperty(TruststorePasswordSetter.TRUSTSTORE_PASSWORD_SYSTEM_PROPERTY)); + } + Assert.assertNull(System.getProperty(TruststorePasswordSetter.TRUSTSTORE_PASSWORD_SYSTEM_PROPERTY)); + } +} diff --git a/gateway-util-common/src/test/java/org/apache/knox/gateway/util/X509CertificateUtilTest.java b/gateway-util-common/src/test/java/org/apache/knox/gateway/util/X509CertificateUtilTest.java new file mode 100644 index 0000000000..06365f575e --- /dev/null +++ b/gateway-util-common/src/test/java/org/apache/knox/gateway/util/X509CertificateUtilTest.java @@ -0,0 +1,94 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.knox.gateway.util; + +import org.junit.Assert; +import org.junit.Test; + +import javax.net.ssl.KeyManagerFactory; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLServerSocket; +import javax.net.ssl.SSLServerSocketFactory; +import javax.net.ssl.SSLSocket; +import java.io.IOException; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.KeyStore; +import java.security.SecureRandom; +import java.security.cert.X509Certificate; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; + +public class X509CertificateUtilTest { + + @Test + public void testFetchPublicCertsFromServer() throws Exception { + // 1. Generate a self-signed certificate + KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA"); + keyPairGenerator.initialize(2048); + KeyPair keyPair = keyPairGenerator.generateKeyPair(); + X509Certificate cert = X509CertificateUtil.generateCertificate("CN=localhost", keyPair, 30, "SHA256withRSA"); + + // 2. Set up a KeyStore with the certificate + KeyStore keyStore = KeyStore.getInstance("JKS"); + keyStore.load(null, null); + keyStore.setKeyEntry("alias", keyPair.getPrivate(), "password".toCharArray(), new java.security.cert.Certificate[]{cert}); + + KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); + kmf.init(keyStore, "password".toCharArray()); + + SSLContext serverSslContext = SSLContext.getInstance("TLS"); + serverSslContext.init(kmf.getKeyManagers(), null, new SecureRandom()); + + SSLServerSocketFactory ssf = serverSslContext.getServerSocketFactory(); + try (SSLServerSocket serverSocket = (SSLServerSocket) ssf.createServerSocket(0)) { + int port = serverSocket.getLocalPort(); + + ExecutorService executor = Executors.newSingleThreadExecutor(); + Future serverFuture = executor.submit(new Callable() { + @Override + public Void call() throws Exception { + try (SSLSocket clientSocket = (SSLSocket) serverSocket.accept()) { + clientSocket.startHandshake(); + } catch (IOException e) { + // Expected if client closes connection early + } + return null; + } + }); + + // 3. Fetch the certificate from the server + try { + String serverUrl = "https://localhost:" + port; + X509Certificate[] certs = X509CertificateUtil.fetchPublicCertsFromServer(serverUrl, null, true, null); + + // 4. Verify + Assert.assertNotNull(certs); + Assert.assertTrue(certs.length > 0); + Assert.assertEquals(cert.getSubjectX500Principal(), certs[0].getSubjectX500Principal()); + Assert.assertEquals(cert.getPublicKey(), certs[0].getPublicKey()); + } finally { + serverFuture.get(5, TimeUnit.SECONDS); + executor.shutdown(); + } + } + } +} From b85ddb0d12ae5a903afeda6ad773a558266ac669 Mon Sep 17 00:00:00 2001 From: Sandor Molnar Date: Thu, 16 Apr 2026 08:47:08 +0200 Subject: [PATCH 03/14] KnoxIDF - Initial commit --- .../resources/build-tools/spotbugs-filter.xml | 5 + .../jwt/filter/SSOCookieFederationFilter.java | 3 +- .../applications/knoxauth/app/js/knoxauth.js | 37 +- .../applications/knoxauth/app/login.html | 8 +- .../applications/knoxauth/app/styles/knox.css | 67 ++++ .../src/main/resources/docker/Dockerfile | 1 + .../resources/docker/gateway-entrypoint.sh | 7 + .../gateway/filter/AnonymousAuthFilter.java | 1 + .../jwt/filter/JWTFederationFilter.java | 13 +- .../jwt/filter/SSOCookieFederationFilter.java | 24 +- .../federation/SSOCookieProviderTest.java | 3 +- .../gateway/filter/RedirectToUrlFilter.java | 2 +- .../home/conf/topologies/knoxsso.xml | 4 + gateway-release/home/conf/users.ldif | 20 +- gateway-release/pom.xml | 4 + .../knox/gateway/UrlEncodedFormRequest.java | 14 +- .../database/AbstractDataSourceFactory.java | 8 + .../knox/gateway/database/DatabaseType.java | 39 +- .../gateway/deploy/DeploymentFactory.java | 9 + .../services/DefaultGatewayServices.java | 2 + .../FederatedIdentityServiceFactory.java | 64 ++++ .../EmptyFederatedIdentitityService.java | 51 +++ .../JdbcFederatedIdentityService.java | 227 +++++++++++ ...pache.knox.gateway.services.ServiceFactory | 7 +- ...xcloakFederatedIdentityAttributesTable.sql | 21 + ...kFederatedIdentityAttributesTableDerby.sql | 22 ++ ...FederatedIdentityAttributesTableOracle.sql | 22 ++ .../createKnoxcloakFederatedIdentityTable.sql | 25 ++ ...teKnoxcloakFederatedIdentityTableDerby.sql | 25 ++ ...eKnoxcloakFederatedIdentityTableOracle.sql | 25 ++ .../services/AbstractGatewayServicesTest.java | 3 +- gateway-service-knoxidf/pom.xml | 115 ++++++ .../service/knoxidf/AuthConsentServlet.java | 132 +++++++ .../knoxidf/AuthSuccessRedirectServlet.java | 94 +++++ .../service/knoxidf/AuthorizeResource.java | 358 ++++++++++++++++++ .../service/knoxidf/DiscoveryResource.java | 68 ++++ .../gateway/service/knoxidf/JwksResource.java | 39 ++ .../knoxidf/LdapUserParamsProvider.java | 165 ++++++++ .../gateway/service/knoxidf/OIDCScope.java | 66 ++++ .../service/knoxidf/RegistrationResource.java | 153 ++++++++ .../service/knoxidf/TokenResource.java | 252 ++++++++++++ .../service/knoxidf/UserInfoResource.java | 137 +++++++ .../service/knoxidf/UserParamsProvider.java | 30 ++ .../KnoxIDFServiceDeploymentContributor.java | 42 ++ ...ateway.deploy.ServiceDeploymentContributor | 18 + .../service/knoxsso/WebSSOResource.java | 74 ++-- .../knoxtoken/ClientCredentialsResource.java | 6 + .../service/knoxtoken/TokenResource.java | 65 +++- .../security/CommonTokenConstants.java | 2 + .../knox/gateway/services/ServiceType.java | 3 +- .../knoxidf/federation/FederatedIdentity.java | 87 +++++ .../federation/FederatedIdentityService.java | 34 ++ .../security/token/JWTokenAttributes.java | 234 ++++++------ .../token/JWTokenAttributesBuilder.java | 17 +- .../security/token/TokenMetadata.java | 10 +- .../security/token/TokenMetadataType.java | 2 +- .../services/security/token/impl/JWT.java | 3 + .../security/token/impl/JWTToken.java | 17 + gateway-util-common/pom.xml | 17 + .../apache/knox/gateway/util/JsonUtils.java | 9 +- .../knoxidf/AuthorizeRequestMetadata.java | 108 ++++++ .../AuthorizeRequestMetadataStore.java | 58 +++ .../knoxidf/FederatedOpConfiguration.java | 81 ++++ .../FederatedOpConfigurationFactory.java | 28 ++ .../util/knoxidf/KnoxIDFConstants.java | 53 +++ .../gateway/util/knoxidf/KnoxIDFUtils.java | 109 ++++++ pom.xml | 17 + 67 files changed, 3271 insertions(+), 195 deletions(-) create mode 100644 gateway-server/src/main/java/org/apache/knox/gateway/services/factory/FederatedIdentityServiceFactory.java create mode 100644 gateway-server/src/main/java/org/apache/knox/gateway/services/knoxidf/federation/EmptyFederatedIdentitityService.java create mode 100644 gateway-server/src/main/java/org/apache/knox/gateway/services/knoxidf/federation/JdbcFederatedIdentityService.java create mode 100644 gateway-server/src/main/resources/createKnoxcloakFederatedIdentityAttributesTable.sql create mode 100644 gateway-server/src/main/resources/createKnoxcloakFederatedIdentityAttributesTableDerby.sql create mode 100644 gateway-server/src/main/resources/createKnoxcloakFederatedIdentityAttributesTableOracle.sql create mode 100644 gateway-server/src/main/resources/createKnoxcloakFederatedIdentityTable.sql create mode 100644 gateway-server/src/main/resources/createKnoxcloakFederatedIdentityTableDerby.sql create mode 100644 gateway-server/src/main/resources/createKnoxcloakFederatedIdentityTableOracle.sql create mode 100644 gateway-service-knoxidf/pom.xml create mode 100644 gateway-service-knoxidf/src/main/java/org/apache/knox/gateway/service/knoxidf/AuthConsentServlet.java create mode 100644 gateway-service-knoxidf/src/main/java/org/apache/knox/gateway/service/knoxidf/AuthSuccessRedirectServlet.java create mode 100644 gateway-service-knoxidf/src/main/java/org/apache/knox/gateway/service/knoxidf/AuthorizeResource.java create mode 100644 gateway-service-knoxidf/src/main/java/org/apache/knox/gateway/service/knoxidf/DiscoveryResource.java create mode 100644 gateway-service-knoxidf/src/main/java/org/apache/knox/gateway/service/knoxidf/JwksResource.java create mode 100644 gateway-service-knoxidf/src/main/java/org/apache/knox/gateway/service/knoxidf/LdapUserParamsProvider.java create mode 100644 gateway-service-knoxidf/src/main/java/org/apache/knox/gateway/service/knoxidf/OIDCScope.java create mode 100644 gateway-service-knoxidf/src/main/java/org/apache/knox/gateway/service/knoxidf/RegistrationResource.java create mode 100644 gateway-service-knoxidf/src/main/java/org/apache/knox/gateway/service/knoxidf/TokenResource.java create mode 100644 gateway-service-knoxidf/src/main/java/org/apache/knox/gateway/service/knoxidf/UserInfoResource.java create mode 100644 gateway-service-knoxidf/src/main/java/org/apache/knox/gateway/service/knoxidf/UserParamsProvider.java create mode 100644 gateway-service-knoxidf/src/main/java/org/apache/knox/gateway/service/knoxidf/deploy/KnoxIDFServiceDeploymentContributor.java create mode 100644 gateway-service-knoxidf/src/main/resources/META-INF/services/org.apache.knox.gateway.deploy.ServiceDeploymentContributor create mode 100644 gateway-spi/src/main/java/org/apache/knox/gateway/services/knoxidf/federation/FederatedIdentity.java create mode 100644 gateway-spi/src/main/java/org/apache/knox/gateway/services/knoxidf/federation/FederatedIdentityService.java create mode 100644 gateway-util-common/src/main/java/org/apache/knox/gateway/util/knoxidf/AuthorizeRequestMetadata.java create mode 100644 gateway-util-common/src/main/java/org/apache/knox/gateway/util/knoxidf/AuthorizeRequestMetadataStore.java create mode 100644 gateway-util-common/src/main/java/org/apache/knox/gateway/util/knoxidf/FederatedOpConfiguration.java create mode 100644 gateway-util-common/src/main/java/org/apache/knox/gateway/util/knoxidf/FederatedOpConfigurationFactory.java create mode 100644 gateway-util-common/src/main/java/org/apache/knox/gateway/util/knoxidf/KnoxIDFConstants.java create mode 100644 gateway-util-common/src/main/java/org/apache/knox/gateway/util/knoxidf/KnoxIDFUtils.java diff --git a/build-tools/src/main/resources/build-tools/spotbugs-filter.xml b/build-tools/src/main/resources/build-tools/spotbugs-filter.xml index 1af440809d..dc1ab640c2 100644 --- a/build-tools/src/main/resources/build-tools/spotbugs-filter.xml +++ b/build-tools/src/main/resources/build-tools/spotbugs-filter.xml @@ -90,4 +90,9 @@ limitations under the License. + + + + + diff --git a/gateway-adapter/src/main/java/org/apache/hadoop/gateway/provider/federation/jwt/filter/SSOCookieFederationFilter.java b/gateway-adapter/src/main/java/org/apache/hadoop/gateway/provider/federation/jwt/filter/SSOCookieFederationFilter.java index d4e4340721..b1f6696d6a 100644 --- a/gateway-adapter/src/main/java/org/apache/hadoop/gateway/provider/federation/jwt/filter/SSOCookieFederationFilter.java +++ b/gateway-adapter/src/main/java/org/apache/hadoop/gateway/provider/federation/jwt/filter/SSOCookieFederationFilter.java @@ -19,6 +19,7 @@ import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; +import java.io.UnsupportedEncodingException; /** * An adapter class that delegate calls to @@ -46,7 +47,7 @@ protected void handleValidationError(HttpServletRequest request, * @return url to use as login url for redirect */ @Override - protected String constructLoginURL(HttpServletRequest request) { + protected String constructLoginURL(HttpServletRequest request) throws UnsupportedEncodingException { return super.constructLoginURL(request); } } diff --git a/gateway-applications/src/main/resources/applications/knoxauth/app/js/knoxauth.js b/gateway-applications/src/main/resources/applications/knoxauth/app/js/knoxauth.js index 50379c68d4..b4fa53ceed 100644 --- a/gateway-applications/src/main/resources/applications/knoxauth/app/js/knoxauth.js +++ b/gateway-applications/src/main/resources/applications/knoxauth/app/js/knoxauth.js @@ -16,7 +16,8 @@ */ var loginPageSuffix = "/knoxauth/login.html"; -var webssoURL = "/api/v1/websso?originalUrl="; +var webssoURLBase = "/api/v1/websso"; +var webssoURL = webssoURLBase + "?originalUrl="; var userAgent = navigator.userAgent.toLowerCase(); function get(name) { @@ -26,6 +27,11 @@ function get(name) { } } +function getSimpleParam(name) { + const params = new URLSearchParams(window.location.search); + return params.get(name); +} + function testSameOrigin(url) { var loc = window.location, a = document.createElement('a'); @@ -55,6 +61,35 @@ var keypressed = function(event) { } }; +var loadFederatedOpLinks = function() { + const ops = getSimpleParam("federatedOpNames")?.split(",") ?? []; + const container = $("#federated-op-container"); + + if (ops.length > 0) { + container.before(` +
+ Or +
+ `); + } + + ops.forEach(op => { + container.append(` +
+ 🌐 + Continue with ${op} +
+ `); + }); +}; + +var loginWithOp = function(opName) { + const sessionId = getSimpleParam("federatedOpLoginSession"); + var pathname = window.location.pathname; + var topologyContext = pathname.replace(loginPageSuffix, ""); + redirect(topologyContext + webssoURLBase + "/federated/op?fedOpSid=" + sessionId + "&fedOpName=" + encodeURIComponent(opName)); +}; + var login = function() { var pathname = window.location.pathname; var topologyContext = pathname.replace(loginPageSuffix, ""); diff --git a/gateway-applications/src/main/resources/applications/knoxauth/app/login.html b/gateway-applications/src/main/resources/applications/knoxauth/app/login.html index bd90cdebb8..5151b98781 100644 --- a/gateway-applications/src/main/resources/applications/knoxauth/app/login.html +++ b/gateway-applications/src/main/resources/applications/knoxauth/app/login.html @@ -31,7 +31,7 @@ - + @@ -90,7 +90,7 @@ - +
@@ -116,5 +116,9 @@
+
+ +
+ diff --git a/gateway-applications/src/main/resources/applications/knoxauth/app/styles/knox.css b/gateway-applications/src/main/resources/applications/knoxauth/app/styles/knox.css index 27fae58fff..1da0548c25 100644 --- a/gateway-applications/src/main/resources/applications/knoxauth/app/styles/knox.css +++ b/gateway-applications/src/main/resources/applications/knoxauth/app/styles/knox.css @@ -1913,4 +1913,71 @@ input[type="radio"], input[type="checkbox"] {margin-top: 0;} margin-left: -5px; margin-top: -2px; font-size: 11px; +} + +.or-separator { + display: flex; + align-items: center; + text-align: center; + margin: 20px auto; + width: 250px; /* or whatever fits your design */ + color: #888; + font-family: sans-serif; + font-size: 14px; +} + +.or-separator::before, +.or-separator::after { + content: ""; + flex: 1; + height: 1px; + background: #ccc; +} + +.or-separator::before { + margin-right: 8px; +} + +.or-separator::after { + margin-left: 8px; +} + +#federated-op-container { + margin: 20px auto 0; /* auto left/right centers it */ + display: flex; + flex-direction: column; + gap: 12px; + width: fit-content; /* shrink to fit content */ +} + +.fed-op-btn { + display: flex; + align-items: center; + gap: 12px; + padding: 10px 16px; + border-radius: 6px; + background: #f7f7f7; + border: 1px solid #d0d0d0; + cursor: pointer; + font-family: sans-serif; + font-size: 15px; + transition: background 0.2s, transform 0.1s; + user-select: none; +} + +.fed-op-btn:hover { + background: #ececec; +} + +.fed-op-btn:active { + transform: scale(0.97); +} + +.fed-op-icon { + font-size: 18px; +} + +.fed-op-label { + flex: 1; + text-align: left; } \ No newline at end of file diff --git a/gateway-docker/src/main/resources/docker/Dockerfile b/gateway-docker/src/main/resources/docker/Dockerfile index 052ac3d12e..91b5d82017 100644 --- a/gateway-docker/src/main/resources/docker/Dockerfile +++ b/gateway-docker/src/main/resources/docker/Dockerfile @@ -12,6 +12,7 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +FROM docker-private.infra.cloudera.com/cloudera_base/hardened/cloudera-openjdk:jdk-17-runtime-nofips FROM dhi.io/eclipse-temurin:17-jdk-debian13-dev AS build LABEL maintainer="Apache Knox " diff --git a/gateway-docker/src/main/resources/docker/gateway-entrypoint.sh b/gateway-docker/src/main/resources/docker/gateway-entrypoint.sh index 2cb0277a3d..c0fb8ac99a 100755 --- a/gateway-docker/src/main/resources/docker/gateway-entrypoint.sh +++ b/gateway-docker/src/main/resources/docker/gateway-entrypoint.sh @@ -249,5 +249,12 @@ done export KNOX_GATEWAY_DBG_OPTS="${KNOX_GATEWAY_DBG_OPTS} -Djavax.net.ssl.trustStore=${KEYSTORE_DIR}/truststore.jks -Djavax.net.ssl.trustStorePassword=${ALIAS_PASSPHRASE}" +# Setup DB for knoxidf demo +/home/knox/knox/bin/knoxcli.sh create-alias gateway_database_user --value postgres +/home/knox/knox/bin/knoxcli.sh create-alias gateway_database_password --value myPassword + +# Setup Knox token hash key for the demo (so that I can re-use the same clientId everywhere) +/home/knox/knox/bin/knoxcli.sh create-alias knox.token.hash.key --value B5oxlx9M4h4MhaGakj8k7Q2fbmPzo6h9te8dWTHs5Mg + echo "Starting Knox gateway ..." /home/knox/knox/bin/gateway.sh start diff --git a/gateway-provider-security-authc-anon/src/main/java/org/apache/knox/gateway/filter/AnonymousAuthFilter.java b/gateway-provider-security-authc-anon/src/main/java/org/apache/knox/gateway/filter/AnonymousAuthFilter.java index b0b2a85d8a..19fd64cc1e 100755 --- a/gateway-provider-security-authc-anon/src/main/java/org/apache/knox/gateway/filter/AnonymousAuthFilter.java +++ b/gateway-provider-security-authc-anon/src/main/java/org/apache/knox/gateway/filter/AnonymousAuthFilter.java @@ -76,6 +76,7 @@ private void continueWithEstablishedSecurityContext(Subject subject, final HttpS new PrivilegedExceptionAction() { @Override public Object run() throws Exception { + request.getParameter("code"); chain.doFilter(request, response); return null; } diff --git a/gateway-provider-security-jwt/src/main/java/org/apache/knox/gateway/provider/federation/jwt/filter/JWTFederationFilter.java b/gateway-provider-security-jwt/src/main/java/org/apache/knox/gateway/provider/federation/jwt/filter/JWTFederationFilter.java index 31b3df4112..098f28247a 100644 --- a/gateway-provider-security-jwt/src/main/java/org/apache/knox/gateway/provider/federation/jwt/filter/JWTFederationFilter.java +++ b/gateway-provider-security-jwt/src/main/java/org/apache/knox/gateway/provider/federation/jwt/filter/JWTFederationFilter.java @@ -22,6 +22,7 @@ import org.apache.knox.gateway.i18n.messages.MessagesFactory; import org.apache.knox.gateway.provider.federation.jwt.JWTMessages; import org.apache.knox.gateway.security.PrimaryPrincipal; +import org.apache.knox.gateway.services.security.token.TokenUtils; import org.apache.knox.gateway.services.security.token.UnknownTokenException; import org.apache.knox.gateway.services.security.token.impl.JWT; import org.apache.knox.gateway.services.security.token.impl.JWTToken; @@ -48,10 +49,11 @@ import java.util.Set; import static java.nio.charset.StandardCharsets.UTF_8; -import static org.apache.knox.gateway.security.CommonTokenConstants.GRANT_TYPE; +import static org.apache.knox.gateway.security.CommonTokenConstants.AUTH_CODE; import static org.apache.knox.gateway.security.CommonTokenConstants.CLIENT_CREDENTIALS; import static org.apache.knox.gateway.security.CommonTokenConstants.CLIENT_ID; import static org.apache.knox.gateway.security.CommonTokenConstants.CLIENT_SECRET; +import static org.apache.knox.gateway.security.CommonTokenConstants.GRANT_TYPE; import static org.apache.knox.gateway.util.AuthFilterUtils.DEFAULT_AUTH_UNAUTHENTICATED_PATHS_PARAM; public class JWTFederationFilter extends AbstractJWTFilter { @@ -189,6 +191,10 @@ public void doFilter(ServletRequest request, ServletResponse response, FilterCha JWT token = new JWTToken(tokenValue); if (validateToken((HttpServletRequest) request, (HttpServletResponse) response, chain, token)) { Subject subject = createSubjectFromToken(token); + request.setAttribute("X-Token-Id", TokenUtils.getTokenId(token)); + if (token.getClaim("scope") != null) { + request.setAttribute("X-Token-Scope", token.getClaim("scope")); + } continueWithEstablishedSecurityContext(subject, (HttpServletRequest) request, (HttpServletResponse) response, chain); } } catch (ParseException | UnknownTokenException ex) { @@ -211,6 +217,7 @@ public void doFilter(ServletRequest request, ServletResponse response, FilterCha if (validateToken((HttpServletRequest) request, (HttpServletResponse) response, chain, tokenId, passcode)) { try { Subject subject = createSubjectFromTokenIdentifier(tokenId); + request.setAttribute("X-Token-Id", tokenId); continueWithEstablishedSecurityContext(subject, (HttpServletRequest) request, (HttpServletResponse) response, chain); } catch (UnknownTokenException e) { ((HttpServletResponse) response).sendError(HttpServletResponse.SC_UNAUTHORIZED); @@ -322,8 +329,8 @@ private Pair getTokenFromRequestBody(ServletRequest request) HttpServletRequest unwrappedRequest = ServletRequestUtils.unwrapHttpServletRequest(request); final String grantType = unwrappedRequest.getParameter(GRANT_TYPE); final String clientAssertionType = unwrappedRequest.getParameter(CLIENT_ASSERTION_TYPE); - if (CLIENT_CREDENTIALS.equals(grantType)) { - if (clientAssertionType != null && CLIENT_ASSERTION_JWT_BEARER.equals(clientAssertionType)) { + if (CLIENT_CREDENTIALS.equals(grantType) || AUTH_CODE.equals(grantType)) { + if (CLIENT_ASSERTION_JWT_BEARER.equals(clientAssertionType)) { // short lived client assertion token expected return getClientTokenFromParams(unwrappedRequest, CLIENT_ASSERTION); } diff --git a/gateway-provider-security-jwt/src/main/java/org/apache/knox/gateway/provider/federation/jwt/filter/SSOCookieFederationFilter.java b/gateway-provider-security-jwt/src/main/java/org/apache/knox/gateway/provider/federation/jwt/filter/SSOCookieFederationFilter.java index a8e7b8f8de..b42d50d435 100644 --- a/gateway-provider-security-jwt/src/main/java/org/apache/knox/gateway/provider/federation/jwt/filter/SSOCookieFederationFilter.java +++ b/gateway-provider-security-jwt/src/main/java/org/apache/knox/gateway/provider/federation/jwt/filter/SSOCookieFederationFilter.java @@ -31,6 +31,9 @@ import org.apache.knox.gateway.util.CertificateUtils; import org.apache.knox.gateway.util.CookieUtils; import org.apache.knox.gateway.util.Urls; +import org.apache.knox.gateway.util.knoxidf.AuthorizeRequestMetadataStore; +import org.apache.knox.gateway.util.knoxidf.FederatedOpConfiguration; +import org.apache.knox.gateway.util.knoxidf.KnoxIDFUtils; import org.eclipse.jetty.http.MimeTypes; import javax.security.auth.Subject; @@ -43,14 +46,18 @@ import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; +import java.io.UnsupportedEncodingException; import java.net.MalformedURLException; import java.net.URL; +import java.net.URLEncoder; import java.nio.charset.StandardCharsets; import java.text.ParseException; import java.util.ArrayList; import java.util.HashSet; import java.util.List; +import java.util.Map; import java.util.Set; +import java.util.stream.Collectors; public class SSOCookieFederationFilter extends AbstractJWTFilter { private static final JWTMessages LOGGER = MessagesFactory.get( JWTMessages.class ); @@ -103,6 +110,7 @@ public class SSOCookieFederationFilter extends AbstractJWTFilter { private boolean shouldUseOriginalUrlFromHeader = DEFAULT_SHOULD_USE_ORIGINAL_URL_FROM_HEADER; private boolean verifyOriginalUrlFromHeaderDomain = DEFAULT_VERIFY_ORIGINAL_URL_FROM_HEADER_DOMAIN; private final List verifyOriginalUrlFromHeaderDomainWhitelist = new ArrayList<>(); + private final AuthorizeRequestMetadataStore authorizeRequestMetadataStore = AuthorizeRequestMetadataStore.getInstance(120000L); private String originalUrlHeaderName; @Override @@ -324,7 +332,7 @@ private String constructGlobalLogoutUrl(HttpServletRequest request) { * @param request for getting the original request URL * @return url to use as login url for redirect */ - protected String constructLoginURL(HttpServletRequest request) { + protected String constructLoginURL(HttpServletRequest request) throws UnsupportedEncodingException { String providerURL = null; String delimiter = "?"; if (authenticationProviderUrl == null) { @@ -337,6 +345,20 @@ protected String constructLoginURL(HttpServletRequest request) { delimiter = "&"; } + final Set enabledFederatedOpConfigs = KnoxIDFUtils.fetchEnabledFederatedOpConfigs(request); + if (!enabledFederatedOpConfigs.isEmpty()) { + final String loginSessionId = request.getSession().getId(); + final Map federatedOpMap = enabledFederatedOpConfigs.stream(). + collect(Collectors.toMap(FederatedOpConfiguration::getName, config -> config)); + authorizeRequestMetadataStore.storeFederatedOpConfiguration(loginSessionId, federatedOpMap); + authorizeRequestMetadataStore.storeRequestMetadata(loginSessionId, KnoxIDFUtils.buildAuthRequestMetadata(request)); + providerURL += delimiter + + "federatedOpLoginSession=" + URLEncoder.encode(loginSessionId, "UTF-8") + + "&federatedOpNames=" + URLEncoder.encode(String.join(",", federatedOpMap.keySet()), "UTF-8"); + + delimiter = "&"; + } + if(shouldUseOriginalUrlFromHeader && (request.getHeader(originalUrlHeaderName) != null) && !request.getHeader(originalUrlHeaderName).trim().isEmpty()) { final String originalUrlFromHeader = request.getHeader(originalUrlHeaderName); LOGGER.usingOriginalUrlFromHeader(originalUrlFromHeader); diff --git a/gateway-provider-security-jwt/src/test/java/org/apache/knox/gateway/provider/federation/SSOCookieProviderTest.java b/gateway-provider-security-jwt/src/test/java/org/apache/knox/gateway/provider/federation/SSOCookieProviderTest.java index 90c056f2ea..f3457fb621 100644 --- a/gateway-provider-security-jwt/src/test/java/org/apache/knox/gateway/provider/federation/SSOCookieProviderTest.java +++ b/gateway-provider-security-jwt/src/test/java/org/apache/knox/gateway/provider/federation/SSOCookieProviderTest.java @@ -21,6 +21,7 @@ import static org.apache.knox.gateway.provider.federation.jwt.filter.SSOCookieFederationFilter.XHR_VALUE; import static org.junit.Assert.fail; +import java.io.UnsupportedEncodingException; import java.nio.charset.StandardCharsets; import java.security.Principal; import java.time.Instant; @@ -738,7 +739,7 @@ private static class TestSSOCookieFederationProvider extends SSOCookieFederation private int verificationCount; @Override - public String constructLoginURL(HttpServletRequest req) { + public String constructLoginURL(HttpServletRequest req) throws UnsupportedEncodingException { return super.constructLoginURL(req); } diff --git a/gateway-provider-security-shiro/src/main/java/org/apache/knox/gateway/filter/RedirectToUrlFilter.java b/gateway-provider-security-shiro/src/main/java/org/apache/knox/gateway/filter/RedirectToUrlFilter.java index f0f14b365d..7501dbe4b4 100644 --- a/gateway-provider-security-shiro/src/main/java/org/apache/knox/gateway/filter/RedirectToUrlFilter.java +++ b/gateway-provider-security-shiro/src/main/java/org/apache/knox/gateway/filter/RedirectToUrlFilter.java @@ -47,7 +47,7 @@ public void init(FilterConfig filterConfig) throws ServletException { @Override protected void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException { - if (redirectUrl != null && request.getHeader("Authorization") == null) { + if (redirectUrl != null && request.getHeader("Authorization") == null && request.getParameter("fedOpSid") == null) { response.sendRedirect(redirectUrl + getOriginalQueryString(request)); } chain.doFilter(request, response); diff --git a/gateway-release/home/conf/topologies/knoxsso.xml b/gateway-release/home/conf/topologies/knoxsso.xml index 99600f8746..cfee258c34 100644 --- a/gateway-release/home/conf/topologies/knoxsso.xml +++ b/gateway-release/home/conf/topologies/knoxsso.xml @@ -73,6 +73,10 @@ main.ldapRealm.contextFactory.authenticationMechanism simple + + urls./api/v1/websso/federated/op + anon + urls./** authcBasic diff --git a/gateway-release/home/conf/users.ldif b/gateway-release/home/conf/users.ldif index 986704dc40..c148b6865b 100644 --- a/gateway-release/home/conf/users.ldif +++ b/gateway-release/home/conf/users.ldif @@ -39,7 +39,9 @@ objectclass:organizationalPerson objectclass:inetOrgPerson cn: Guest sn: User +givenName: Guest uid: guest +mail: guest@example.org userPassword:guest-password # entry for sample user admin @@ -48,9 +50,11 @@ objectclass:top objectclass:person objectclass:organizationalPerson objectclass:inetOrgPerson -cn: Admin -sn: Admin +cn: System Administrator +sn: Administrator +givenName: System uid: admin +mail: admin@example.org userPassword:admin-password # entry for sample user sam @@ -59,9 +63,11 @@ objectclass:top objectclass:person objectclass:organizationalPerson objectclass:inetOrgPerson -cn: sam -sn: sam +cn: Sam Peterson +sn: Peterson +givenName: Sam uid: sam +mail: sam@example.org userPassword:sam-password # entry for sample user tom @@ -70,9 +76,11 @@ objectclass:top objectclass:person objectclass:organizationalPerson objectclass:inetOrgPerson -cn: tom -sn: tom +cn: Tom Richards +sn: Richards +givenName: Tom uid: tom +mail: tom@example.org userPassword:tom-password # create FIRST Level groups branch diff --git a/gateway-release/pom.xml b/gateway-release/pom.xml index c8b7c74d42..391c1a25ba 100644 --- a/gateway-release/pom.xml +++ b/gateway-release/pom.xml @@ -524,5 +524,9 @@ org.apache.knox gateway-service-restcatalog + + org.apache.knox + gateway-service-knoxidf + diff --git a/gateway-server/src/main/java/org/apache/knox/gateway/UrlEncodedFormRequest.java b/gateway-server/src/main/java/org/apache/knox/gateway/UrlEncodedFormRequest.java index 2e2482aa1d..139e51862b 100644 --- a/gateway-server/src/main/java/org/apache/knox/gateway/UrlEncodedFormRequest.java +++ b/gateway-server/src/main/java/org/apache/knox/gateway/UrlEncodedFormRequest.java @@ -17,17 +17,17 @@ */ package org.apache.knox.gateway; +import org.apache.knox.gateway.i18n.messages.MessagesFactory; +import org.eclipse.jetty.util.MultiMap; +import org.eclipse.jetty.util.UrlEncoded; + +import javax.servlet.ServletRequest; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletRequestWrapper; import java.io.IOException; import java.util.Enumeration; import java.util.Iterator; import java.util.Map; -import javax.servlet.ServletRequest; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletRequestWrapper; - -import org.apache.knox.gateway.i18n.messages.MessagesFactory; -import org.eclipse.jetty.util.MultiMap; -import org.eclipse.jetty.util.UrlEncoded; /** * HttpServletRequest diff --git a/gateway-server/src/main/java/org/apache/knox/gateway/database/AbstractDataSourceFactory.java b/gateway-server/src/main/java/org/apache/knox/gateway/database/AbstractDataSourceFactory.java index 7a7afadd13..6cfefc7dd4 100644 --- a/gateway-server/src/main/java/org/apache/knox/gateway/database/AbstractDataSourceFactory.java +++ b/gateway-server/src/main/java/org/apache/knox/gateway/database/AbstractDataSourceFactory.java @@ -42,6 +42,14 @@ public abstract class AbstractDataSourceFactory { public static final String DERBY_KNOX_PROVIDERS_TABLE_CREATE_SQL_FILE_NAME = "createKnoxProvidersTableDerby.sql"; public static final String DERBY_KNOX_DESCRIPTORS_TABLE_CREATE_SQL_FILE_NAME = "createKnoxDescriptorsTableDerby.sql"; + //KNOXIDF + public static final String KNOXIDF_FED_IDENTITY_TABLE_CREATE_SQL_FILE_NAME = "createKnoxcloakFederatedIdentityTable.sql"; + public static final String KNOXIDF_FED_IDENTITY_ATTR_TABLE_CREATE_SQL_FILE_NAME = "createKnoxcloakFederatedIdentityAttributesTable.sql"; + public static final String ORACLE_KNOXIDF_FED_IDENTITY_TABLE_CREATE_SQL_FILE_NAME = "createKnoxcloakFederatedIdentityTableOracle.sql"; + public static final String ORACLE_KNOXIDF_FED_IDENTITY_ATTR_TABLE_CREATE_SQL_FILE_NAME = "createKnoxcloakFederatedIdentityAttributesTableOracle.sql"; + public static final String DERBY_KNOXIDF_FED_IDENTITY_TABLE_CREATE_SQL_FILE_NAME = "createKnoxcloakFederatedIdentityTableDerby.sql"; + public static final String DERBY_KNOXIDF_FED_IDENTITY_ATTR_TABLE_CREATE_SQL_FILE_NAME = "createKnoxcloakFederatedIdentityAttributesTableDerby.sql"; + public static final String DATABASE_USER_ALIAS_NAME = "gateway_database_user"; public static final String DATABASE_PASSWORD_ALIAS_NAME = "gateway_database_password"; public static final String DATABASE_TRUSTSTORE_PASSWORD_ALIAS_NAME = "gateway_database_ssl_truststore_password"; diff --git a/gateway-server/src/main/java/org/apache/knox/gateway/database/DatabaseType.java b/gateway-server/src/main/java/org/apache/knox/gateway/database/DatabaseType.java index 2009872782..3f627bdac8 100644 --- a/gateway-server/src/main/java/org/apache/knox/gateway/database/DatabaseType.java +++ b/gateway-server/src/main/java/org/apache/knox/gateway/database/DatabaseType.java @@ -22,37 +22,50 @@ public enum DatabaseType { AbstractDataSourceFactory.POSTGRES_TOKENS_TABLE_CREATE_SQL_FILE_NAME, AbstractDataSourceFactory.POSTGRES_TOKEN_METADATA_TABLE_CREATE_SQL_FILE_NAME, AbstractDataSourceFactory.KNOX_PROVIDERS_TABLE_CREATE_SQL_FILE_NAME, - AbstractDataSourceFactory.KNOX_DESCRIPTORS_TABLE_CREATE_SQL_FILE_NAME + AbstractDataSourceFactory.KNOX_DESCRIPTORS_TABLE_CREATE_SQL_FILE_NAME, + AbstractDataSourceFactory.KNOXIDF_FED_IDENTITY_TABLE_CREATE_SQL_FILE_NAME, + AbstractDataSourceFactory.KNOXIDF_FED_IDENTITY_ATTR_TABLE_CREATE_SQL_FILE_NAME ), MYSQL("mysql", AbstractDataSourceFactory.TOKENS_TABLE_CREATE_SQL_FILE_NAME, AbstractDataSourceFactory.TOKEN_METADATA_TABLE_CREATE_SQL_FILE_NAME, AbstractDataSourceFactory.KNOX_PROVIDERS_TABLE_CREATE_SQL_FILE_NAME, - AbstractDataSourceFactory.KNOX_DESCRIPTORS_TABLE_CREATE_SQL_FILE_NAME + AbstractDataSourceFactory.KNOX_DESCRIPTORS_TABLE_CREATE_SQL_FILE_NAME, + AbstractDataSourceFactory.KNOXIDF_FED_IDENTITY_TABLE_CREATE_SQL_FILE_NAME, + AbstractDataSourceFactory.KNOXIDF_FED_IDENTITY_ATTR_TABLE_CREATE_SQL_FILE_NAME ), MARIADB("mariadb", AbstractDataSourceFactory.TOKENS_TABLE_CREATE_SQL_FILE_NAME, AbstractDataSourceFactory.TOKEN_METADATA_TABLE_CREATE_SQL_FILE_NAME, AbstractDataSourceFactory.KNOX_PROVIDERS_TABLE_CREATE_SQL_FILE_NAME, - AbstractDataSourceFactory.KNOX_DESCRIPTORS_TABLE_CREATE_SQL_FILE_NAME + AbstractDataSourceFactory.KNOX_DESCRIPTORS_TABLE_CREATE_SQL_FILE_NAME, + AbstractDataSourceFactory.KNOXIDF_FED_IDENTITY_TABLE_CREATE_SQL_FILE_NAME, + AbstractDataSourceFactory.KNOXIDF_FED_IDENTITY_ATTR_TABLE_CREATE_SQL_FILE_NAME ), HSQL("hsql", AbstractDataSourceFactory.TOKENS_TABLE_CREATE_SQL_FILE_NAME, AbstractDataSourceFactory.TOKEN_METADATA_TABLE_CREATE_SQL_FILE_NAME, AbstractDataSourceFactory.KNOX_PROVIDERS_TABLE_CREATE_SQL_FILE_NAME, - AbstractDataSourceFactory.KNOX_DESCRIPTORS_TABLE_CREATE_SQL_FILE_NAME + AbstractDataSourceFactory.KNOX_DESCRIPTORS_TABLE_CREATE_SQL_FILE_NAME, + AbstractDataSourceFactory.KNOXIDF_FED_IDENTITY_TABLE_CREATE_SQL_FILE_NAME, + AbstractDataSourceFactory.KNOXIDF_FED_IDENTITY_ATTR_TABLE_CREATE_SQL_FILE_NAME ), DERBY("derbydb", AbstractDataSourceFactory.DERBY_TOKENS_TABLE_CREATE_SQL_FILE_NAME, AbstractDataSourceFactory.DERBY_TOKEN_METADATA_TABLE_CREATE_SQL_FILE_NAME, AbstractDataSourceFactory.DERBY_KNOX_PROVIDERS_TABLE_CREATE_SQL_FILE_NAME, - AbstractDataSourceFactory.DERBY_KNOX_DESCRIPTORS_TABLE_CREATE_SQL_FILE_NAME + AbstractDataSourceFactory.DERBY_KNOX_DESCRIPTORS_TABLE_CREATE_SQL_FILE_NAME, + AbstractDataSourceFactory.DERBY_KNOXIDF_FED_IDENTITY_TABLE_CREATE_SQL_FILE_NAME, + AbstractDataSourceFactory.DERBY_KNOXIDF_FED_IDENTITY_ATTR_TABLE_CREATE_SQL_FILE_NAME + ), ORACLE("oracle", AbstractDataSourceFactory.ORACLE_TOKENS_TABLE_CREATE_SQL_FILE_NAME, AbstractDataSourceFactory.ORACLE_TOKEN_METADATA_TABLE_CREATE_SQL_FILE_NAME, AbstractDataSourceFactory.ORACLE_KNOX_PROVIDERS_TABLE_CREATE_SQL_FILE_NAME, - AbstractDataSourceFactory.ORACLE_KNOX_DESCRIPTORS_TABLE_CREATE_SQL_FILE_NAME + AbstractDataSourceFactory.ORACLE_KNOX_DESCRIPTORS_TABLE_CREATE_SQL_FILE_NAME, + AbstractDataSourceFactory.ORACLE_KNOXIDF_FED_IDENTITY_TABLE_CREATE_SQL_FILE_NAME, + AbstractDataSourceFactory.ORACLE_KNOXIDF_FED_IDENTITY_ATTR_TABLE_CREATE_SQL_FILE_NAME ); private final String type; @@ -60,13 +73,17 @@ public enum DatabaseType { private final String metadataTableSql; private final String providersTableSql; private final String descriptorsTableSql; + private final String federatedIdentityTableSql; + private final String federatedIdentityAttrTableSql; - DatabaseType(String type, String tokensTableSql, String metadataTableSql, String providersTableSql, String descriptorsTableSql) { + DatabaseType(String type, String tokensTableSql, String metadataTableSql, String providersTableSql, String descriptorsTableSql, String federatedIdentityTableSql, String federatedIdentityAttrTableSql) { this.type = type; this.tokensTableSql = tokensTableSql; this.metadataTableSql = metadataTableSql; this.providersTableSql = providersTableSql; this.descriptorsTableSql = descriptorsTableSql; + this.federatedIdentityTableSql = federatedIdentityTableSql; + this.federatedIdentityAttrTableSql = federatedIdentityAttrTableSql; } public String type() { @@ -89,6 +106,14 @@ public String descriptorsTableSql() { return descriptorsTableSql; } + public String federatedIdentityTableSql() { + return federatedIdentityTableSql; + } + + public String federatedIdentityAttrTableSql() { + return federatedIdentityAttrTableSql; + } + public static DatabaseType fromString(String dbType) { for (DatabaseType dt : values()) { if (dt.type.equalsIgnoreCase(dbType)) { diff --git a/gateway-server/src/main/java/org/apache/knox/gateway/deploy/DeploymentFactory.java b/gateway-server/src/main/java/org/apache/knox/gateway/deploy/DeploymentFactory.java index dfe4a4ea90..242bfb4d52 100644 --- a/gateway-server/src/main/java/org/apache/knox/gateway/deploy/DeploymentFactory.java +++ b/gateway-server/src/main/java/org/apache/knox/gateway/deploy/DeploymentFactory.java @@ -376,6 +376,15 @@ private static void initialize( GatewayConfig gatewayConfig) { WebAppDescriptor wad = context.getWebAppDescriptor(); String topoName = context.getTopology().getName(); + + final boolean hasKnoxIdf = services!= null && services.entrySet().stream().anyMatch( e -> e.getKey().equalsIgnoreCase("KNOXIDF") ); + if (hasKnoxIdf) { + wad.createServlet().servletName("auth-success-redirect").servletClass("org.apache.knox.gateway.service.knoxidf.AuthSuccessRedirectServlet"); + wad.createServletMapping().servletName("auth-success-redirect").urlPattern("/authSuccess"); + wad.createServlet().servletName("auth-consent-redirect").servletClass("org.apache.knox.gateway.service.knoxidf.AuthConsentServlet"); + wad.createServletMapping().servletName("auth-consent-redirect").urlPattern("/authConsent"); + } + boolean asyncSupported = gatewayConfig.isAsyncSupported() || gatewayConfig.isTopologyAsyncSupported(topoName); if( applications == null ) { String servletName = topoName + SERVLET_NAME_SUFFIX; diff --git a/gateway-server/src/main/java/org/apache/knox/gateway/services/DefaultGatewayServices.java b/gateway-server/src/main/java/org/apache/knox/gateway/services/DefaultGatewayServices.java index 06ce95d93a..cdbafc7bbb 100644 --- a/gateway-server/src/main/java/org/apache/knox/gateway/services/DefaultGatewayServices.java +++ b/gateway-server/src/main/java/org/apache/knox/gateway/services/DefaultGatewayServices.java @@ -90,6 +90,8 @@ public void init(GatewayConfig config, Map options) throws Servic ldapService.init(config, options); addService(ServiceType.LDAP_SERVICE, ldapService); } + + addService(ServiceType.KNOXIDF_FEDERATED_IDENTITY_SERVICE, gatewayServiceFactory.create(this, ServiceType.KNOXIDF_FEDERATED_IDENTITY_SERVICE, config, options)); } @Override diff --git a/gateway-server/src/main/java/org/apache/knox/gateway/services/factory/FederatedIdentityServiceFactory.java b/gateway-server/src/main/java/org/apache/knox/gateway/services/factory/FederatedIdentityServiceFactory.java new file mode 100644 index 0000000000..f38648fd66 --- /dev/null +++ b/gateway-server/src/main/java/org/apache/knox/gateway/services/factory/FederatedIdentityServiceFactory.java @@ -0,0 +1,64 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package org.apache.knox.gateway.services.factory; + +import org.apache.knox.gateway.config.GatewayConfig; +import org.apache.knox.gateway.services.GatewayServices; +import org.apache.knox.gateway.services.Service; +import org.apache.knox.gateway.services.ServiceLifecycleException; +import org.apache.knox.gateway.services.ServiceType; +import org.apache.knox.gateway.services.knoxidf.federation.EmptyFederatedIdentitityService; +import org.apache.knox.gateway.services.knoxidf.federation.FederatedIdentityService; +import org.apache.knox.gateway.services.knoxidf.federation.JdbcFederatedIdentityService; + +import java.util.Collection; +import java.util.List; +import java.util.Map; + +public class FederatedIdentityServiceFactory extends AbstractServiceFactory { + + private static final String DEFAULT_IMPLEMENTATION = EmptyFederatedIdentitityService.class.getName(); + + @Override + protected Service createService(GatewayServices gatewayServices, ServiceType serviceType, GatewayConfig gatewayConfig, Map options, String implementation) + throws ServiceLifecycleException { + FederatedIdentityService service = null; + if (shouldCreateService(implementation)) { + if (matchesImplementation(implementation, EmptyFederatedIdentitityService.class, true)) { + service = new EmptyFederatedIdentitityService(); + } else if (matchesImplementation(implementation, JdbcFederatedIdentityService.class)) { + try { + service = new JdbcFederatedIdentityService(gatewayConfig, getAliasService(gatewayServices)); + } catch (Exception e) { + throw new ServiceLifecycleException("Error while creating Federated Identity Service: " + e, e); + } + } + logServiceUsage(implementation, serviceType); + } + return service; + } + + @Override + protected ServiceType getServiceType() { + return ServiceType.KNOXIDF_FEDERATED_IDENTITY_SERVICE; + } + + @Override + protected Collection getKnownImplementations() { + return List.of(DEFAULT_IMPLEMENTATION, JdbcFederatedIdentityService.class.getName()); + } +} diff --git a/gateway-server/src/main/java/org/apache/knox/gateway/services/knoxidf/federation/EmptyFederatedIdentitityService.java b/gateway-server/src/main/java/org/apache/knox/gateway/services/knoxidf/federation/EmptyFederatedIdentitityService.java new file mode 100644 index 0000000000..8ce9e550e9 --- /dev/null +++ b/gateway-server/src/main/java/org/apache/knox/gateway/services/knoxidf/federation/EmptyFederatedIdentitityService.java @@ -0,0 +1,51 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package org.apache.knox.gateway.services.knoxidf.federation; + +import org.apache.knox.gateway.config.GatewayConfig; +import org.apache.knox.gateway.services.ServiceLifecycleException; + +import java.util.Map; +import java.util.Optional; + +public class EmptyFederatedIdentitityService implements FederatedIdentityService { + @Override + public void addFederatedIdentity(FederatedIdentity identity) { + } + + @Override + public Optional findById(String identityId) { + return Optional.empty(); + } + + @Override + public Optional findByProviderAndSubject(String provider, String externalIssuer, String externalSubject) { + return Optional.empty(); + } + + @Override + public void init(GatewayConfig config, Map options) throws ServiceLifecycleException { + } + + @Override + public void start() throws ServiceLifecycleException { + } + + @Override + public void stop() throws ServiceLifecycleException { + } +} diff --git a/gateway-server/src/main/java/org/apache/knox/gateway/services/knoxidf/federation/JdbcFederatedIdentityService.java b/gateway-server/src/main/java/org/apache/knox/gateway/services/knoxidf/federation/JdbcFederatedIdentityService.java new file mode 100644 index 0000000000..cf426b8369 --- /dev/null +++ b/gateway-server/src/main/java/org/apache/knox/gateway/services/knoxidf/federation/JdbcFederatedIdentityService.java @@ -0,0 +1,227 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package org.apache.knox.gateway.services.knoxidf.federation; + +import org.apache.knox.gateway.config.GatewayConfig; +import org.apache.knox.gateway.database.DataSourceProvider; +import org.apache.knox.gateway.database.DatabaseType; +import org.apache.knox.gateway.database.JDBCUtils; +import org.apache.knox.gateway.services.ServiceLifecycleException; +import org.apache.knox.gateway.services.security.AliasService; +import org.apache.knox.gateway.services.token.impl.TokenStateDatabase; + +import javax.sql.DataSource; +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Timestamp; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; + +//TODO: split this class into service + FederatedIdentityDatabase classes +public class JdbcFederatedIdentityService implements FederatedIdentityService { + private static final String FEDERATED_IDENTITY_TABLE_NAME = "federated_identity"; + private static final String FEDERATED_IDENTITY_ATTRIBUTES_TABLE_NAME = "federated_identity_attr"; + + private final AtomicBoolean initialized = new AtomicBoolean(false); + private final Lock initLock = new ReentrantLock(true); + private final DataSource dataSource; + + public JdbcFederatedIdentityService(GatewayConfig config, AliasService aliasService) throws Exception { + this.dataSource = DataSourceProvider.getDataSource(config, aliasService); + } + + private void ensureTablesExist(DatabaseType databaseType) { + try { + createTableIfNotExists(FEDERATED_IDENTITY_TABLE_NAME, databaseType.federatedIdentityTableSql()); + createTableIfNotExists(FEDERATED_IDENTITY_ATTRIBUTES_TABLE_NAME, databaseType.federatedIdentityAttrTableSql()); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + private void createTableIfNotExists(String tableName, String createSqlFileName) throws Exception { + if (!JDBCUtils.tableExists(tableName, dataSource)) { + JDBCUtils.createTableFromSQL(createSqlFileName, dataSource, TokenStateDatabase.class.getClassLoader()); + } + } + + @Override + public void init(GatewayConfig config, Map options) throws ServiceLifecycleException { + if (!initialized.get()) { + initLock.lock(); + try { + ensureTablesExist(DatabaseType.fromString(config.getDatabaseType())); + } finally { + initLock.unlock(); + } + } + } + + @Override + public void start() throws ServiceLifecycleException { + } + + @Override + public void stop() throws ServiceLifecycleException { + } + + @Override + public void addFederatedIdentity(FederatedIdentity identity) { + try (Connection c = dataSource.getConnection()) { + c.setAutoCommit(false); + + try (PreparedStatement ps = c.prepareStatement( + "INSERT INTO federated_identity " + + "(id, user_id, provider, external_subject, external_issuer, created_at) " + + "VALUES (?, ?, ?, ?, ?, ?)")) { + + ps.setString(1, identity.getId()); + ps.setString(2, identity.getUserId()); + ps.setString(3, identity.getProvider()); + ps.setString(4, identity.getExternalSubject()); + ps.setString(5, identity.getExternalIssuer()); + ps.setTimestamp(6, Timestamp.from(identity.getCreatedAt())); + ps.executeUpdate(); + } + + try (PreparedStatement ps = c.prepareStatement( + "INSERT INTO federated_identity_attr " + + "(identity_id, attr_key, attr_value) VALUES (?, ?, ?)")) { + + for (var e : identity.getAttributes().entrySet()) { + ps.setString(1, identity.getId()); + ps.setString(2, e.getKey()); + ps.setString(3, e.getValue()); + ps.addBatch(); + } + ps.executeBatch(); + } + + c.commit(); + } catch (SQLException e) { + throw new RuntimeException(e); + } + } + + @Override + public Optional findByProviderAndSubject( + String provider, + String issuer, + String subject) { + + try (Connection c = dataSource.getConnection()) { + + FederatedIdentity federatedIdentity = null; + + try (PreparedStatement ps = c.prepareStatement("SELECT * FROM federated_identity " + + "WHERE provider = ? AND external_issuer = ? AND external_subject = ?")) { + ps.setString(1, provider); + ps.setString(2, issuer); + ps.setString(3, subject); + + try (ResultSet rs = ps.executeQuery()) { + if (!rs.next()) { + return Optional.empty(); + } + + federatedIdentity = new FederatedIdentity( + rs.getString("id"), + rs.getString("user_id"), + provider, + subject, + issuer, + rs.getTimestamp("created_at").toInstant(), new HashMap<>()); + } + } + + try (PreparedStatement ps = c.prepareStatement( + "SELECT attr_key, attr_value FROM federated_identity_attr WHERE identity_id = ?")) { + + ps.setString(1, federatedIdentity.getId()); + + try (ResultSet rs = ps.executeQuery()) { + while (rs.next()) { + federatedIdentity.getAttributes().put( + rs.getString(1), + rs.getString(2)); + } + } + } + + return Optional.of(federatedIdentity); + } catch (SQLException e) { + throw new RuntimeException(e); + } + } + + @Override + public Optional findById(String id) { + + try (Connection c = dataSource.getConnection()) { + + FederatedIdentity federatedIdentity; + + try (PreparedStatement ps = c.prepareStatement( + "SELECT id, user_id, provider, external_subject, external_issuer, created_at " + + "FROM federated_identity WHERE id = ?")) { + + ps.setString(1, id); + + try (ResultSet rs = ps.executeQuery()) { + if (!rs.next()) { + return Optional.empty(); + } + + federatedIdentity = new FederatedIdentity( + rs.getString("id"), + rs.getString("user_id"), + rs.getString("provider"), + rs.getString("external_subject"), + rs.getString("external_issuer"), + rs.getTimestamp("created_at").toInstant(), + new HashMap<>()); + } + } + + try (PreparedStatement ps = c.prepareStatement( + "SELECT attr_key, attr_value FROM federated_identity_attr WHERE identity_id = ?")) { + + ps.setString(1, federatedIdentity.getId()); + + try (ResultSet rs = ps.executeQuery()) { + while (rs.next()) { + federatedIdentity.getAttributes().put( + rs.getString("attr_key"), + rs.getString("attr_value")); + } + } + } + + return Optional.of(federatedIdentity); + + } catch (SQLException e) { + throw new RuntimeException("Failed to load federated identity by id", e); + } + } + +} diff --git a/gateway-server/src/main/resources/META-INF/services/org.apache.knox.gateway.services.ServiceFactory b/gateway-server/src/main/resources/META-INF/services/org.apache.knox.gateway.services.ServiceFactory index 4c015979f9..83f6b04ee3 100644 --- a/gateway-server/src/main/resources/META-INF/services/org.apache.knox.gateway.services.ServiceFactory +++ b/gateway-server/src/main/resources/META-INF/services/org.apache.knox.gateway.services.ServiceFactory @@ -17,8 +17,11 @@ ########################################################################## org.apache.knox.gateway.services.factory.AliasServiceFactory +org.apache.knox.gateway.services.factory.ConcurrentSessionVerifierFactory org.apache.knox.gateway.services.factory.ClusterConfigurationMonitorServiceFactory org.apache.knox.gateway.services.factory.CryptoServiceFactory +org.apache.knox.gateway.services.factory.FederatedIdentityServiceFactory +org.apache.knox.gateway.services.factory.GatewayStatusServiceFactory org.apache.knox.gateway.services.factory.HostMappingServiceFactory org.apache.knox.gateway.services.factory.KeystoreServiceFactory org.apache.knox.gateway.services.factory.MasterServiceFactory @@ -28,8 +31,6 @@ org.apache.knox.gateway.services.factory.ServerInfoServiceFactory org.apache.knox.gateway.services.factory.ServiceDefinitionRegistryFactory org.apache.knox.gateway.services.factory.ServiceRegistryServiceFactory org.apache.knox.gateway.services.factory.SslServiceFactory -org.apache.knox.gateway.services.factory.TokenServiceFactory org.apache.knox.gateway.services.factory.TokenStateServiceFactory org.apache.knox.gateway.services.factory.TopologyServiceFactory -org.apache.knox.gateway.services.factory.ConcurrentSessionVerifierFactory -org.apache.knox.gateway.services.factory.GatewayStatusServiceFactory \ No newline at end of file +org.apache.knox.gateway.services.factory.TokenServiceFactory \ No newline at end of file diff --git a/gateway-server/src/main/resources/createKnoxcloakFederatedIdentityAttributesTable.sql b/gateway-server/src/main/resources/createKnoxcloakFederatedIdentityAttributesTable.sql new file mode 100644 index 0000000000..239cb5df1a --- /dev/null +++ b/gateway-server/src/main/resources/createKnoxcloakFederatedIdentityAttributesTable.sql @@ -0,0 +1,21 @@ +-- Licensed to the Apache Software Foundation (ASF) under one or more +-- contributor license agreements. See the NOTICE file distributed with this +-- work for additional information regarding copyright ownership. The ASF +-- licenses this file to you under the Apache License, Version 2.0 (the +-- "License"); you may not use this file except in compliance with the License. +-- You may obtain a copy of the License at +-- +-- http://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +-- WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +-- License for the specific language governing permissions and limitations under +-- the License. +CREATE TABLE FEDERATED_IDENTITY_ATTR ( + identity_id VARCHAR(36) NOT NULL, + attr_key VARCHAR(128) NOT NULL, + attr_value TEXT, + PRIMARY KEY (identity_id, attr_key), + FOREIGN KEY (identity_id) REFERENCES FEDERATED_IDENTITY (id) ON DELETE CASCADE +); \ No newline at end of file diff --git a/gateway-server/src/main/resources/createKnoxcloakFederatedIdentityAttributesTableDerby.sql b/gateway-server/src/main/resources/createKnoxcloakFederatedIdentityAttributesTableDerby.sql new file mode 100644 index 0000000000..13ae9ab113 --- /dev/null +++ b/gateway-server/src/main/resources/createKnoxcloakFederatedIdentityAttributesTableDerby.sql @@ -0,0 +1,22 @@ +-- Licensed to the Apache Software Foundation (ASF) under one or more +-- contributor license agreements. See the NOTICE file distributed with this +-- work for additional information regarding copyright ownership. The ASF +-- licenses this file to you under the Apache License, Version 2.0 (the +-- "License"); you may not use this file except in compliance with the License. +-- You may obtain a copy of the License at +-- +-- http://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +-- WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +-- License for the specific language governing permissions and limitations under +-- the License. + +CREATE TABLE FEDERATED_IDENTITY_ATTR ( + identity_id VARCHAR(36), + attr_key VARCHAR(128), + attr_value CLOB, + PRIMARY KEY (identity_id, attr_key), + CONSTRAINT fk_fed_attr FOREIGN KEY (identity_id) REFERENCES FEDERATED_IDENTITY(id) ON DELETE CASCADE +); \ No newline at end of file diff --git a/gateway-server/src/main/resources/createKnoxcloakFederatedIdentityAttributesTableOracle.sql b/gateway-server/src/main/resources/createKnoxcloakFederatedIdentityAttributesTableOracle.sql new file mode 100644 index 0000000000..7ed07b1b05 --- /dev/null +++ b/gateway-server/src/main/resources/createKnoxcloakFederatedIdentityAttributesTableOracle.sql @@ -0,0 +1,22 @@ +-- Licensed to the Apache Software Foundation (ASF) under one or more +-- contributor license agreements. See the NOTICE file distributed with this +-- work for additional information regarding copyright ownership. The ASF +-- licenses this file to you under the Apache License, Version 2.0 (the +-- "License"); you may not use this file except in compliance with the License. +-- You may obtain a copy of the License at +-- +-- http://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +-- WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +-- License for the specific language governing permissions and limitations under +-- the License. + +CREATE TABLE FEDERATED_IDENTITY_ATTR ( + identity_id VARCHAR2(36) NOT NULL, + attr_key VARCHAR2(128) NOT NULL, + attr_value CLOB, + CONSTRAINT pk_fed_attr PRIMARY KEY (identity_id, attr_key), + CONSTRAINT fk_fed_attr FOREIGN KEY (identity_id) REFERENCES FEDERATED_IDENTITY(id) ON DELETE CASCADE +); \ No newline at end of file diff --git a/gateway-server/src/main/resources/createKnoxcloakFederatedIdentityTable.sql b/gateway-server/src/main/resources/createKnoxcloakFederatedIdentityTable.sql new file mode 100644 index 0000000000..acaf1c3404 --- /dev/null +++ b/gateway-server/src/main/resources/createKnoxcloakFederatedIdentityTable.sql @@ -0,0 +1,25 @@ +-- Licensed to the Apache Software Foundation (ASF) under one or more +-- contributor license agreements. See the NOTICE file distributed with this +-- work for additional information regarding copyright ownership. The ASF +-- licenses this file to you under the Apache License, Version 2.0 (the +-- "License"); you may not use this file except in compliance with the License. +-- You may obtain a copy of the License at +-- +-- http://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +-- WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +-- License for the specific language governing permissions and limitations under +-- the License. + +CREATE TABLE FEDERATED_IDENTITY ( + id VARCHAR(36) PRIMARY KEY, + user_id VARCHAR(36) NOT NULL, + provider VARCHAR(64) NOT NULL, + external_subject VARCHAR(255) NOT NULL, + external_issuer VARCHAR(255) NOT NULL, + created_at TIMESTAMP NOT NULL +); + +CREATE UNIQUE INDEX UX_FED_IDENTITY ON FEDERATED_IDENTITY (provider, external_issuer, external_subject); \ No newline at end of file diff --git a/gateway-server/src/main/resources/createKnoxcloakFederatedIdentityTableDerby.sql b/gateway-server/src/main/resources/createKnoxcloakFederatedIdentityTableDerby.sql new file mode 100644 index 0000000000..7152d4c71d --- /dev/null +++ b/gateway-server/src/main/resources/createKnoxcloakFederatedIdentityTableDerby.sql @@ -0,0 +1,25 @@ +-- Licensed to the Apache Software Foundation (ASF) under one or more +-- contributor license agreements. See the NOTICE file distributed with this +-- work for additional information regarding copyright ownership. The ASF +-- licenses this file to you under the Apache License, Version 2.0 (the +-- "License"); you may not use this file except in compliance with the License. +-- You may obtain a copy of the License at +-- +-- http://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +-- WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +-- License for the specific language governing permissions and limitations under +-- the License. + +CREATE TABLE FEDERATED_IDENTITY ( + id VARCHAR(36) PRIMARY KEY, + user_id VARCHAR(36), + provider VARCHAR(64), + external_subject VARCHAR(255), + external_issuer VARCHAR(255), + created_at TIMESTAMP +); + +CREATE UNIQUE INDEX UX_FED_IDENTITY ON FEDERATED_IDENTITY (provider, external_issuer, external_subject); \ No newline at end of file diff --git a/gateway-server/src/main/resources/createKnoxcloakFederatedIdentityTableOracle.sql b/gateway-server/src/main/resources/createKnoxcloakFederatedIdentityTableOracle.sql new file mode 100644 index 0000000000..0dc42c1f72 --- /dev/null +++ b/gateway-server/src/main/resources/createKnoxcloakFederatedIdentityTableOracle.sql @@ -0,0 +1,25 @@ +-- Licensed to the Apache Software Foundation (ASF) under one or more +-- contributor license agreements. See the NOTICE file distributed with this +-- work for additional information regarding copyright ownership. The ASF +-- licenses this file to you under the Apache License, Version 2.0 (the +-- "License"); you may not use this file except in compliance with the License. +-- You may obtain a copy of the License at +-- +-- http://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +-- WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +-- License for the specific language governing permissions and limitations under +-- the License. + +CREATE TABLE FEDERATED_IDENTITY ( + id VARCHAR2(36) PRIMARY KEY, + user_id VARCHAR2(36) NOT NULL, + provider VARCHAR2(64) NOT NULL, + external_subject VARCHAR2(255) NOT NULL, + external_issuer VARCHAR2(255) NOT NULL, + created_at TIMESTAMP NOT NULL +); + +CREATE UNIQUE INDEX UX_FED_IDENTITY ON FEDERATED_IDENTITY (provider, external_issuer, external_subject); \ No newline at end of file diff --git a/gateway-server/src/test/java/org/apache/knox/gateway/services/AbstractGatewayServicesTest.java b/gateway-server/src/test/java/org/apache/knox/gateway/services/AbstractGatewayServicesTest.java index b27358c7cb..1a5483c537 100644 --- a/gateway-server/src/test/java/org/apache/knox/gateway/services/AbstractGatewayServicesTest.java +++ b/gateway-server/src/test/java/org/apache/knox/gateway/services/AbstractGatewayServicesTest.java @@ -66,7 +66,8 @@ public void testAddStartAndStop() throws ServiceLifecycleException { ServiceType.CONCURRENT_SESSION_VERIFIER, ServiceType.REMOTE_CONFIGURATION_MONITOR, ServiceType.GATEWAY_STATUS_SERVICE, - ServiceType.LDAP_SERVICE + ServiceType.LDAP_SERVICE, + ServiceType.KNOXIDF_FEDERATED_IDENTITY_SERVICE }; assertNotEquals(ServiceType.values(), orderedServiceTypes); diff --git a/gateway-service-knoxidf/pom.xml b/gateway-service-knoxidf/pom.xml new file mode 100644 index 0000000000..48296b435d --- /dev/null +++ b/gateway-service-knoxidf/pom.xml @@ -0,0 +1,115 @@ + + + + 4.0.0 + + org.apache.knox + gateway + 3.0.0-SNAPSHOT + + + gateway-service-knoxidf + gateway-service-knoxidf + + + + org.apache.knox + gateway-i18n + + + org.apache.knox + gateway-spi + + + org.apache.knox + gateway-provider-jersey + + + org.apache.knox + gateway-util-common + + + org.apache.knox + gateway-service-knoxtoken + + + + javax.annotation + javax.annotation-api + + + javax.ws.rs + javax.ws.rs-api + + + javax.servlet + javax.servlet-api + + + + com.google.guava + guava + + + commons-io + commons-io + + + com.fasterxml.jackson.core + jackson-core + + + com.fasterxml.jackson.core + jackson-databind + + + com.fasterxml.uuid + java-uuid-generator + + + com.nimbusds + nimbus-jose-jwt + + + com.github.ben-manes.caffeine + caffeine + + + org.apache.commons + commons-lang3 + + + org.apache.commons + commons-text + + + org.apache.httpcomponents + httpclient + + + org.apache.httpcomponents + httpcore + + + org.glassfish.jersey.core + jersey-common + + + diff --git a/gateway-service-knoxidf/src/main/java/org/apache/knox/gateway/service/knoxidf/AuthConsentServlet.java b/gateway-service-knoxidf/src/main/java/org/apache/knox/gateway/service/knoxidf/AuthConsentServlet.java new file mode 100644 index 0000000000..6200edc835 --- /dev/null +++ b/gateway-service-knoxidf/src/main/java/org/apache/knox/gateway/service/knoxidf/AuthConsentServlet.java @@ -0,0 +1,132 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package org.apache.knox.gateway.service.knoxidf; + +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.UriInfo; +import java.io.IOException; +import java.io.PrintWriter; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Locale; +import java.util.Set; + +import static org.apache.knox.gateway.util.knoxidf.KnoxIDFUtils.getRequestParamSafe; + + +public class AuthConsentServlet extends HttpServlet { + + @Context + UriInfo uriInfo; + + @Override + protected void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException { + response.setContentType("text/html;charset=UTF-8"); + final String clientId = getRequestParamSafe(request, "client_id"); + final String state = getRequestParamSafe(request, "state"); + final String scope = getRequestParamSafe(request, "scope"); + final Set scopes = new HashSet<>(Arrays.asList(scope.split("\\s+"))); + + try (PrintWriter out = response.getWriter()) { + out.println(""); + out.println("Consent Required"); + out.println(""); + out.println(""); + out.println("

"); + out.println("

Application Consent Required

"); + out.printf(Locale.US, "

The application %s is requesting access to your account.

%n", clientId); + + if (!scopes.isEmpty()) { + out.println("

This application will be able to:

"); + out.println("
    "); + for (String s : scopes) { + out.printf(Locale.US, "
  • %s
  • %n", describeScope(s)); + } + out.println("
"); + } + + out.println("
"); + out.printf(Locale.US, "%n", state); + out.println("
"); + out.println(""); + out.println(""); + out.println("
"); + out.println("
"); + out.println("
"); + out.println(""); + out.println(""); + } + } + + private String describeScope(String scope) { + if (scope == null) { + return ""; + } + + switch (scope) { + case "openid": + return "Authenticate using your account"; + case "profile": + return "View your basic profile information"; + case "email": + return "View your email address"; + case "address": + return "View your address information"; + case "phone": + return "View your phone number"; + case "calendar.read": + return "Read your calendar events"; + case "calendar.write": + return "Modify your calendar events"; + default: + return scope; + } + } + + //Redirect target is application-local and state is encoded/controlled + @SuppressWarnings("UNVALIDATED_REDIRECT") + @Override + protected void doPost(HttpServletRequest request, HttpServletResponse response) throws IOException { + final String action = request.getParameter("action"); + final String state = request.getParameter("state"); + final String redirectUri = request.getServletContext().getContextPath() + "/" + AuthorizeResource.RESOURCE_PATH + + ("accept".equals(action) ? "/consentAccepted?state=" + state : "/consentDenied"); + response.sendRedirect(redirectUri); + } + +} diff --git a/gateway-service-knoxidf/src/main/java/org/apache/knox/gateway/service/knoxidf/AuthSuccessRedirectServlet.java b/gateway-service-knoxidf/src/main/java/org/apache/knox/gateway/service/knoxidf/AuthSuccessRedirectServlet.java new file mode 100644 index 0000000000..fd9a96a2e4 --- /dev/null +++ b/gateway-service-knoxidf/src/main/java/org/apache/knox/gateway/service/knoxidf/AuthSuccessRedirectServlet.java @@ -0,0 +1,94 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package org.apache.knox.gateway.service.knoxidf; + + +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.io.PrintWriter; + +import static org.apache.knox.gateway.util.knoxidf.KnoxIDFUtils.getRequestParamSafe; + +public class AuthSuccessRedirectServlet extends HttpServlet { + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException { + resp.setContentType("text/html;charset=UTF-8"); + + final String code = getRequestParamSafe(req, "code"); + final String state = getRequestParamSafe(req, "state"); + + try (PrintWriter out = resp.getWriter()) { + out.println(""); + out.println(""); + out.println(""); + out.println(" "); + out.println(" Authentication Success"); + out.println(" "); + out.println(""); + out.println(""); + out.println("

"); + out.println("

✅ Authentication Successful!

"); + out.println("

Your authorization code:

"); + out.println("
" + code + "

"); + out.println(" "); + out.println("
Copied to clipboard!
"); + if (state != null) { + out.println("
"); + out.println("

State:

"); + out.println("
" + state + "
"); + } + out.println("
"); + out.println(" "); + out.println(""); + out.println(""); + } + } + +} diff --git a/gateway-service-knoxidf/src/main/java/org/apache/knox/gateway/service/knoxidf/AuthorizeResource.java b/gateway-service-knoxidf/src/main/java/org/apache/knox/gateway/service/knoxidf/AuthorizeResource.java new file mode 100644 index 0000000000..52197bba0d --- /dev/null +++ b/gateway-service-knoxidf/src/main/java/org/apache/knox/gateway/service/knoxidf/AuthorizeResource.java @@ -0,0 +1,358 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package org.apache.knox.gateway.service.knoxidf; + +import com.fasterxml.uuid.Generators; +import com.fasterxml.uuid.impl.NameBasedGenerator; +import com.nimbusds.jose.KeyLengthException; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.tuple.Pair; +import org.apache.http.NameValuePair; +import org.apache.http.client.entity.UrlEncodedFormEntity; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClients; +import org.apache.http.message.BasicNameValuePair; +import org.apache.http.util.EntityUtils; +import org.apache.knox.gateway.security.SubjectUtils; +import org.apache.knox.gateway.service.knoxtoken.PasscodeTokenResourceBase; +import org.apache.knox.gateway.services.GatewayServices; +import org.apache.knox.gateway.services.ServiceLifecycleException; +import org.apache.knox.gateway.services.ServiceType; +import org.apache.knox.gateway.services.knoxidf.federation.FederatedIdentity; +import org.apache.knox.gateway.services.knoxidf.federation.FederatedIdentityService; +import org.apache.knox.gateway.services.security.AliasServiceException; +import org.apache.knox.gateway.services.security.token.TokenMetadata; +import org.apache.knox.gateway.services.security.token.TokenMetadataType; +import org.apache.knox.gateway.services.security.token.UnknownTokenException; +import org.apache.knox.gateway.services.security.token.impl.JWT; +import org.apache.knox.gateway.services.security.token.impl.JWTToken; +import org.apache.knox.gateway.util.JsonUtils; +import org.apache.knox.gateway.util.knoxidf.AuthorizeRequestMetadata; +import org.apache.knox.gateway.util.knoxidf.AuthorizeRequestMetadataStore; +import org.apache.knox.gateway.util.knoxidf.FederatedOpConfiguration; +import org.apache.knox.gateway.util.knoxidf.KnoxIDFUtils; + +import javax.annotation.PostConstruct; +import javax.servlet.ServletContext; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.QueryParam; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.Response; +import java.io.UnsupportedEncodingException; +import java.net.URI; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.text.ParseException; +import java.time.Instant; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; +import java.util.UUID; +import java.util.stream.Collectors; + +import static org.apache.knox.gateway.util.knoxidf.KnoxIDFConstants.BASE_RESORCE_PATH; +import static org.apache.knox.gateway.util.knoxidf.KnoxIDFConstants.DEFAULT_SCOPES; +import static org.apache.knox.gateway.util.knoxidf.KnoxIDFUtils.error; + + +@Path(AuthorizeResource.RESOURCE_PATH) +public class AuthorizeResource extends PasscodeTokenResourceBase { + static final String RESOURCE_PATH = BASE_RESORCE_PATH + "/authorize"; + private static final UUID KNOX_NAMESPACE = UUID.fromString("6ba7b811-9dad-11d1-80b4-00c04fd430c8"); + private static final NameBasedGenerator UUID_V5 = Generators.nameBasedGenerator(KNOX_NAMESPACE); + public static final Set ALLOWED_CLAIMS = Set.of("preferred_username", "email", "email_verified", + "given_name", "family_name", "name", "locale"); + + private static final String UTF_8 = StandardCharsets.UTF_8.name(); + private FederatedOpConfiguration federatedOpConfiguration; + private AuthorizeRequestMetadataStore authorizeRequestMetadataStore; + + @Context + private HttpServletRequest request; + + @Context + private ServletContext servletContext; + + private FederatedIdentityService federatedIdentityService; + + @PostConstruct + @Override + public void init() throws ServletException, AliasServiceException, ServiceLifecycleException, KeyLengthException { + super.init(); + this.federatedOpConfiguration = new FederatedOpConfiguration(servletContext); + this.authorizeRequestMetadataStore = AuthorizeRequestMetadataStore.getInstance(tokenTTL); + final GatewayServices services = (GatewayServices) servletContext.getAttribute(GatewayServices.GATEWAY_SERVICES_ATTRIBUTE); + federatedIdentityService = services.getService(ServiceType.KNOXIDF_FEDERATED_IDENTITY_SERVICE); + } + + @GET + public Response authorize(@QueryParam("response_type") String responseType, + @QueryParam("client_id") String clientId, + @QueryParam("redirect_uri") String redirectUri, + @QueryParam("scope") String scope, + @QueryParam("state") String state, + @QueryParam("nonce") String nonce) throws Exception { + final String subject = SubjectUtils.getCurrentEffectivePrincipalName(); + final Set requestedScopes = StringUtils.isBlank(scope) ? DEFAULT_SCOPES : new HashSet<>(Arrays.asList(scope.split("\\s+"))); + final AuthorizeRequestMetadata authorizeRequestMetadata = new AuthorizeRequestMetadata(clientId, subject, responseType, redirectUri, requestedScopes, state, nonce); + final Response verificationErrorResponse = verifyParams(authorizeRequestMetadata); + if (verificationErrorResponse != null) { + return verificationErrorResponse; + } + + if (!hasConsent(authorizeRequestMetadata)) { + if ("true".equalsIgnoreCase(request.getParameter("auto_consent"))) { + markConsentAccepted(authorizeRequestMetadata); + } else { + final String consentAuthState = UUID.randomUUID().toString(); + authorizeRequestMetadataStore.storeRequestMetadata(consentAuthState, authorizeRequestMetadata); + final String baseUri = servletContext.getContextPath() + "/authConsent"; + final String scopeParam = URLEncoder.encode(authorizeRequestMetadata.getJoinedRequestedScopes(), StandardCharsets.UTF_8); + final String redirect = String.format(Locale.US, "%s?client_id=%s&state=%s&scope=%s", baseUri, clientId, consentAuthState, scopeParam); + return Response.seeOther(java.net.URI.create(redirect)).build(); + } + } + return getAuthCodeFromKnox(authorizeRequestMetadata, null); + } + + private boolean hasConsent(final AuthorizeRequestMetadata authorizeRequestMetadata) { + try { + final TokenMetadata tokenMetadata = tokenStateService.getTokenMetadata(authorizeRequestMetadata.getClientId()); + final String consentKey = "consentAccepted_" + authorizeRequestMetadata.getSubject(); + final String storedScopes = tokenMetadata.getMetadataMap().get(consentKey); + if (storedScopes == null || storedScopes.isEmpty()) { + return false; + } + final Set storedScopeSet = new HashSet<>(Arrays.asList(storedScopes.split("\\s+"))); + return storedScopeSet.containsAll(authorizeRequestMetadata.getRequestedScopes()); + } catch (UnknownTokenException e) { + //this should not happen as we validated the client_id already + return false; + } + } + + private void markConsentAccepted(AuthorizeRequestMetadata authorizeRequestMetadata) { + final TokenMetadata consentAcceptedMetadata = new TokenMetadata(); + consentAcceptedMetadata.add("consentAccepted_" + authorizeRequestMetadata.getSubject(), authorizeRequestMetadata.getJoinedRequestedScopes()); + tokenStateService.addMetadata(authorizeRequestMetadata.getClientId(), consentAcceptedMetadata); + } + + private Response getAuthCodeFromKnox(final AuthorizeRequestMetadata authorizeRequestMetadata, final Pair federatedTokens) throws Exception { + final Response tokenResponse = getAuthenticationToken(); + if (tokenResponse.getStatus() == Response.Status.OK.getStatusCode()) { + final Map tokenResponseMap = JsonUtils.getMapFromJsonString(tokenResponse.getEntity().toString()); + final String tokenId = tokenResponseMap.get(TOKEN_ID); + decorateAuthCodeToken(tokenId, authorizeRequestMetadata, federatedTokens); + return redirectToAuthSuccess(authorizeRequestMetadata, tokenId); + } + return tokenResponse; + } + + private Response redirectToAuthSuccess(final AuthorizeRequestMetadata authorizeRequestMetadata, final String code) throws UnsupportedEncodingException { + final String redirectLocation = authorizeRequestMetadata.getRedirectUri() + + "?code=" + URLEncoder.encode(code, UTF_8) + + "&state=" + URLEncoder.encode(authorizeRequestMetadata.getState(), UTF_8); + return Response.seeOther(URI.create(redirectLocation)).build(); + } + + @GET + @Path("/callback") + public Response authCallback() throws Exception { + //This is the callback for the federated OP + final String federatedAuthCode = request.getParameter("code"); + final String state = request.getParameter("state"); + final AuthorizeRequestMetadata authorizeRequestMetadata = authorizeRequestMetadataStore.getRequestMetadata(state); + final Pair federatedTokens = exchangeFederatedAuthCodeToTokens(federatedAuthCode); + final FederatedIdentity federatedIdentity = resolveFederatedIdentity(federatedTokens.getLeft()); + return getAuthCodeFromKnox(authorizeRequestMetadata, Pair.of(federatedIdentity.getId(), federatedTokens.getRight())); + } + + @GET + @Path("/consentAccepted") + public Response consentAccepted() throws Exception { + final String state = request.getParameter("state"); + final AuthorizeRequestMetadata authorizeRequestMetadata = authorizeRequestMetadataStore.getRequestMetadata(state); + if (authorizeRequestMetadata == null) { + return error("Consent cannot be accepted", "Invalid state"); + } + markConsentAccepted(authorizeRequestMetadata); + return authorize(authorizeRequestMetadata.getResponseType(), + authorizeRequestMetadata.getClientId(), + authorizeRequestMetadata.getRedirectUri(), + authorizeRequestMetadata.getJoinedRequestedScopes(), + authorizeRequestMetadata.getState(), + authorizeRequestMetadata.getNonce()); + } + + @GET + @Path("/consentDenied") + public Response consentDenied() throws Exception { + return Response.status(Response.Status.FORBIDDEN).entity("Consent denied!").build(); + } + + private void decorateAuthCodeToken(final String tokenId, final AuthorizeRequestMetadata authorizeRequestMetadata, final Pair federatedTokens) throws Exception { + final Map authCodeTokenMap = new HashMap<>(); + authCodeTokenMap.put(TokenMetadata.TYPE, TokenMetadataType.AUTH_CODE.name()); + authCodeTokenMap.put("client_id", authorizeRequestMetadata.getClientId()); + authCodeTokenMap.put("redirect_uri", authorizeRequestMetadata.getRedirectUri()); + authCodeTokenMap.put("userName", authorizeRequestMetadata.getSubject()); + authCodeTokenMap.put("scope", authorizeRequestMetadata.getJoinedRequestedScopes()); + if (StringUtils.isNotBlank(authorizeRequestMetadata.getNonce())) { + authCodeTokenMap.put("nonce", authorizeRequestMetadata.getNonce()); + } + if (federatedTokens != null) { + authCodeTokenMap.put("federated_identity_id", federatedTokens.getLeft()); + authCodeTokenMap.putAll(KnoxIDFUtils.splitFederatedToken(federatedTokens.getRight(), false)); + } + tokenStateService.addMetadata(tokenId, new TokenMetadata(authCodeTokenMap)); + } + + private Response verifyParams(final AuthorizeRequestMetadata authorizeRequestMetadata) { + final Response basicVerificationResponse = authorizeRequestMetadata.verify(); + if (basicVerificationResponse == null) { + final TokenMetadata tokenMetadata; + // Verify client ID + try { + //This is ok for a POC, but we should cache that later + tokenMetadata = tokenStateService.getTokenMetadata(authorizeRequestMetadata.getClientId()); + } catch (UnknownTokenException e) { + return error("invalid_request", "Unknown client_id"); + } + + // Verify redirect URI + final String storedRedirectUris = tokenMetadata.getMetadata("redirect_uris"); + if (StringUtils.isBlank(storedRedirectUris)) { + return error("invalid_request", "Missing stored redirect_uris, cannot authorize the request"); + } + final Set registeredRedirectUris = new HashSet<>(Arrays.asList(storedRedirectUris.split(","))); + if (!matchesRedirectUri(authorizeRequestMetadata.getRedirectUri(), registeredRedirectUris)) { + return error("invalid_request", "Invalid redirect_uri"); + } + + // Verify scope(s) + final String storedAllowedScopes = tokenMetadata.getMetadata("allowed_scopes"); + if (StringUtils.isBlank(storedAllowedScopes)) { + return error("invalid_scope", "Missing stored allowed_scopes, cannot authorize the request"); + } + final Set registeredScopes = new HashSet<>(Arrays.asList(storedAllowedScopes.trim().split("\\s+"))); + if (authorizeRequestMetadata.getRequestedScopes().stream().anyMatch(scope -> !registeredScopes.contains(scope))) { + return error("invalid_scope", "One or more requested scopes are not allowed"); + } + + return null; + } + return basicVerificationResponse; + } + + private boolean matchesRedirectUri(String requestedUri, Set registeredUris) { + for (String registered : registeredUris) { + if (registered.endsWith("*")) { + String prefix = registered.substring(0, registered.length() - 1); + if (requestedUri.startsWith(prefix)) { + return true; + } + } else if (registered.equals(requestedUri)) { + return true; + } + } + return false; + } + + private Pair exchangeFederatedAuthCodeToTokens(String federatedAuthCode) { + String federatedIdToken = null; + String federatedAccessToken = null; + final Response federatedTokenExchangeResponse = fetchFederatedTokens(federatedAuthCode, federatedOpConfiguration.getAuthorizeCallback()); + if (federatedTokenExchangeResponse.getStatus() == Response.Status.OK.getStatusCode()) { + final Map federatedTokenExchangeResponseBodyMap = JsonUtils.getMapFromJsonString((String) federatedTokenExchangeResponse.getEntity()); + federatedIdToken = federatedTokenExchangeResponseBodyMap.get("id_token"); + federatedAccessToken = federatedTokenExchangeResponseBodyMap.get("access_token"); + return Pair.of(federatedIdToken, federatedAccessToken); + } else { + throw new RuntimeException("Error fetching Federated Tokens from Federated Auth Code: " + federatedTokenExchangeResponse.getEntity()); + } + } + + private Response fetchFederatedTokens(final String code, final String redirectUri) { + final List params = new ArrayList<>(); + params.add(new BasicNameValuePair("code", code)); + params.add(new BasicNameValuePair("redirect_uri", redirectUri)); + params.add(new BasicNameValuePair("grant_type", "authorization_code")); + params.add(new BasicNameValuePair("client_id", federatedOpConfiguration.getClientId())); + params.add(new BasicNameValuePair("client_secret", federatedOpConfiguration.getClientSecret())); + + try (CloseableHttpClient httpClient = HttpClients.createDefault()) { + HttpPost post = new HttpPost(federatedOpConfiguration.getTokenEndpoint()); + post.setHeader("Content-Type", "application/x-www-form-urlencoded"); + post.setEntity(new UrlEncodedFormEntity(params, StandardCharsets.UTF_8)); + + try (CloseableHttpResponse response = httpClient.execute(post)) { + int status = response.getStatusLine().getStatusCode(); + String body = EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8); + return Response.status(status).entity(body).build(); + } + } catch (Exception e) { + return Response.status(Response.Status.INTERNAL_SERVER_ERROR).entity("{\"error\":\"" + e.getMessage() + "\"}").build(); + } + } + + private FederatedIdentity resolveFederatedIdentity(String federatedIdToken) throws ParseException { + final JWT jwt = new JWTToken(federatedIdToken); + final String issuer = jwt.getIssuer(); + final String subject = jwt.getSubject(); + return federatedIdentityService.findByProviderAndSubject("KEYCLOAK", issuer, subject).orElseGet(() -> persistFederatedIdentity(jwt)); + } + + private FederatedIdentity persistFederatedIdentity(final JWT jwt) { + final Map attributes = jwt.getJWTClaimsSet().getClaims().entrySet().stream() + .filter(e -> ALLOWED_CLAIMS.contains(e.getKey())) + .filter(e -> e.getValue() != null) + .collect(Collectors.toMap( + Map.Entry::getKey, + e -> String.valueOf(e.getValue()), + (a, b) -> a, // defensive: ignore duplicates + HashMap::new + )); + final FederatedIdentity federatedIdentity = new FederatedIdentity( + deriveKnoxSubject(jwt.getSubject(), jwt.getIssuer()), // internal user id (generated) + "KEYCLOAK", // provider + jwt.getSubject(), // external subject + jwt.getIssuer(), // external issuer + Instant.now(), // createdAt + attributes + ); + + federatedIdentityService.addFederatedIdentity(federatedIdentity); + + return federatedIdentity; + } + + private String deriveKnoxSubject(String subject, String issuer) { + final String name = issuer + "|" + subject; + final UUID uuid = UUID_V5.generate(name.getBytes(StandardCharsets.UTF_8)); + return uuid.toString(); + } +} diff --git a/gateway-service-knoxidf/src/main/java/org/apache/knox/gateway/service/knoxidf/DiscoveryResource.java b/gateway-service-knoxidf/src/main/java/org/apache/knox/gateway/service/knoxidf/DiscoveryResource.java new file mode 100644 index 0000000000..84b6dccc6d --- /dev/null +++ b/gateway-service-knoxidf/src/main/java/org/apache/knox/gateway/service/knoxidf/DiscoveryResource.java @@ -0,0 +1,68 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package org.apache.knox.gateway.service.knoxidf; + +import org.apache.knox.gateway.util.JsonUtils; +import org.apache.knox.gateway.util.knoxidf.FederatedOpConfiguration; + +import javax.annotation.PostConstruct; +import javax.servlet.ServletContext; +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.UriInfo; +import java.util.HashMap; +import java.util.Map; + +import static org.apache.knox.gateway.util.knoxidf.KnoxIDFConstants.BASE_RESORCE_PATH; + +@Path(BASE_RESORCE_PATH + "/.well-known/openid-configuration") +@Produces(MediaType.APPLICATION_JSON) +public class DiscoveryResource { + private FederatedOpConfiguration federatedOpConfiguration; + + @Context + private ServletContext servletContext; + + @PostConstruct + public void init() { + this.federatedOpConfiguration = new FederatedOpConfiguration(servletContext); + } + + @GET + public Response getConfig(@Context UriInfo uriInfo) { + final String baseUrl = uriInfo.getBaseUri().toString(); + final Map config = new HashMap<>(); + config.put("issuer", baseUrl.replaceAll("awc-sso", "awc-token") + "knoxidf"); + config.put("authorization_endpoint", baseUrl .replaceAll("noauth", "sso")+ AuthorizeResource.RESOURCE_PATH); + config.put("token_endpoint", baseUrl + .replaceAll("awc-sso", "awc-token") + .replaceAll("noauth", "token") + TokenResource.RESOURCE_PATH); + config.put("userinfo_endpoint", baseUrl + .replaceAll("awc-sso", "awc-token") + .replaceAll("noauth", "token") + UserInfoResource.RESOURCE_PATH); + config.put("jwks_uri", baseUrl + JwksResource.RESOURCE_PATH); + config.put("response_types_supported", new String[]{"code"}); + config.put("grant_types_supported", new String[]{"authorization_code"}); + config.put("id_token_signing_alg_values_supported", new String[]{"RS256"}); + return Response.ok(JsonUtils.renderAsJsonString(config)).build(); + } + +} diff --git a/gateway-service-knoxidf/src/main/java/org/apache/knox/gateway/service/knoxidf/JwksResource.java b/gateway-service-knoxidf/src/main/java/org/apache/knox/gateway/service/knoxidf/JwksResource.java new file mode 100644 index 0000000000..47f802b04d --- /dev/null +++ b/gateway-service-knoxidf/src/main/java/org/apache/knox/gateway/service/knoxidf/JwksResource.java @@ -0,0 +1,39 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package org.apache.knox.gateway.service.knoxidf; + +import org.apache.knox.gateway.service.knoxtoken.JWKSResource; + +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; + +import static org.apache.knox.gateway.util.knoxidf.KnoxIDFConstants.BASE_RESORCE_PATH; + +@Path(JwksResource.RESOURCE_PATH) +@Produces(MediaType.APPLICATION_JSON) +public class JwksResource extends JWKSResource { + static final String RESOURCE_PATH = BASE_RESORCE_PATH + "/jwks"; + + @GET + public Response getKeys() { + return getJwksResponse(); + } +} + diff --git a/gateway-service-knoxidf/src/main/java/org/apache/knox/gateway/service/knoxidf/LdapUserParamsProvider.java b/gateway-service-knoxidf/src/main/java/org/apache/knox/gateway/service/knoxidf/LdapUserParamsProvider.java new file mode 100644 index 0000000000..0bd567c3fd --- /dev/null +++ b/gateway-service-knoxidf/src/main/java/org/apache/knox/gateway/service/knoxidf/LdapUserParamsProvider.java @@ -0,0 +1,165 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package org.apache.knox.gateway.service.knoxidf; + +import javax.naming.Context; +import javax.naming.NamingEnumeration; +import javax.naming.directory.Attribute; +import javax.naming.directory.Attributes; +import javax.naming.directory.SearchControls; +import javax.naming.directory.SearchResult; +import javax.naming.ldap.InitialLdapContext; +import javax.naming.ldap.LdapContext; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Hashtable; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; + + +public class LdapUserParamsProvider implements UserParamsProvider { + + // === Hardcoded LDAP config for now === + private static final String BASE_DN = "dc=hadoop,dc=apache,dc=org"; + private static final String USER_DN_TEMPLATE = "uid=%s,ou=people," + BASE_DN; + private static final String SYSTEM_USER = "uid=admin,ou=people," + BASE_DN; + private static final String SYSTEM_PASSWORD = "admin-password"; + + private static final String[] ATTRIBUTES = {"cn", "sn", "givenName", "mail"}; + + private final String ldapUrl; + + LdapUserParamsProvider(String ldapUrl) { + this.ldapUrl = ldapUrl; + } + + @Override + public Map getParamsFor(String subjectName, String scope) { + Map userParams = new HashMap<>(); + if ("anonymous".equalsIgnoreCase(subjectName)) { + return userParams; + } + + Set requestedClaims = OIDCScope.claimsForScopes(scope); + + LdapContext ctx = null; + try { + ctx = createSystemContext(); + + String userDn = String.format(Locale.US, USER_DN_TEMPLATE, subjectName); + + SearchControls controls = new SearchControls(); + controls.setSearchScope(SearchControls.OBJECT_SCOPE); + controls.setReturningAttributes(ATTRIBUTES); + + NamingEnumeration results = ctx.search(userDn, "(objectClass=*)", controls); + if (results.hasMore()) { + SearchResult sr = results.next(); + Attributes attrs = sr.getAttributes(); + + // --- OIDC standard claims --- + if (requestedClaims.contains("sub")) { + userParams.put("sub", subjectName); + } + if (requestedClaims.contains("name")) { + userParams.put("name", getAttr(attrs, "cn")); + } + if (requestedClaims.contains("family_name")) { + userParams.put("family_name", getAttr(attrs, "sn")); + } + if (requestedClaims.contains("given_name")) { + userParams.put("given_name", getAttr(attrs, "givenName")); + } + if (requestedClaims.contains("email")) { + userParams.put("email", getAttr(attrs, "mail")); + } + if (requestedClaims.contains("email_verified")) { + userParams.put("email_verified", Boolean.TRUE); + } + + // --- Custom: roles --- + if (requestedClaims.contains("roles")) { + List roles = fetchRoles(ctx, userDn); + userParams.put("roles", roles); + } + } + + } catch (Exception e) { + throw new RuntimeException("Failed to fetch user parameters for " + subjectName, e); + } finally { + closeContext(ctx); + } + + return userParams; + } + + private List fetchRoles(LdapContext ctx, String userDn) throws Exception { + List roles = new ArrayList<>(); + + SearchControls groupControls = new SearchControls(); + groupControls.setSearchScope(SearchControls.ONELEVEL_SCOPE); + groupControls.setReturningAttributes(new String[]{"cn", "member"}); + + String groupsBase = "ou=groups," + BASE_DN; + NamingEnumeration groupResults = + ctx.search(groupsBase, "(objectClass=groupOfNames)", groupControls); + + while (groupResults.hasMore()) { + SearchResult group = groupResults.next(); + Attributes groupAttrs = group.getAttributes(); + Attribute members = groupAttrs.get("member"); + if (members != null) { + NamingEnumeration e = members.getAll(); + while (e.hasMore()) { + String memberDn = (String) e.next(); + if (memberDn.equalsIgnoreCase(userDn)) { + roles.add(getAttr(groupAttrs, "cn")); + break; + } + } + } + } + return roles; + } + + private LdapContext createSystemContext() throws Exception { + Hashtable env = new Hashtable<>(); + env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory"); + env.put(Context.PROVIDER_URL, ldapUrl); + env.put(Context.SECURITY_AUTHENTICATION, "simple"); + env.put(Context.SECURITY_PRINCIPAL, SYSTEM_USER); + env.put(Context.SECURITY_CREDENTIALS, SYSTEM_PASSWORD); + return new InitialLdapContext(env, null); + } + + private String getAttr(Attributes attrs, String attrName) throws Exception { + Attribute attr = attrs.get(attrName); + return attr != null ? (String) attr.get() : null; + } + + private void closeContext(LdapContext ctx) { + if (ctx != null) { + try { + ctx.close(); + } catch (Exception ignored) { + } + } + } +} + diff --git a/gateway-service-knoxidf/src/main/java/org/apache/knox/gateway/service/knoxidf/OIDCScope.java b/gateway-service-knoxidf/src/main/java/org/apache/knox/gateway/service/knoxidf/OIDCScope.java new file mode 100644 index 0000000000..d673f3f454 --- /dev/null +++ b/gateway-service-knoxidf/src/main/java/org/apache/knox/gateway/service/knoxidf/OIDCScope.java @@ -0,0 +1,66 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package org.apache.knox.gateway.service.knoxidf; + +import org.apache.commons.lang3.StringUtils; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.Locale; +import java.util.Set; + +public enum OIDCScope { + OPENID(new HashSet<>(Collections.singletonList("sub"))), + PROFILE(new HashSet<>(Arrays.asList( + "name", "family_name", "given_name", + "middle_name", "nickname", "preferred_username", + "profile", "picture", "website", "gender", + "birthdate", "zoneinfo", "locale", "updated_at" + ))), + EMAIL(new HashSet<>(Arrays.asList("email", "email_verified"))), + ADDRESS(new HashSet<>(Collections.singletonList("address"))), + PHONE(new HashSet<>(Arrays.asList("phone_number", "phone_number_verified"))), + ROLES(new HashSet<>(Collections.singletonList("roles"))); // custom extension + + private final Set claims; + + OIDCScope(Set claims) { + this.claims = Collections.unmodifiableSet(new HashSet<>(claims)); + } + + public Set getClaims() { + return claims; + } + + public static Set claimsForScopes(String scopeString) { + Set result = new HashSet<>(); + if (StringUtils.isEmpty(scopeString)) { + return result; + } + + for (String s : scopeString.split("\\s+")) { + try { + OIDCScope scope = OIDCScope.valueOf(s.toUpperCase(Locale.US)); + result.addAll(scope.getClaims()); + } catch (IllegalArgumentException ignored) { + // ignore unknown scopes + } + } + return result; + } +} diff --git a/gateway-service-knoxidf/src/main/java/org/apache/knox/gateway/service/knoxidf/RegistrationResource.java b/gateway-service-knoxidf/src/main/java/org/apache/knox/gateway/service/knoxidf/RegistrationResource.java new file mode 100644 index 0000000000..05675e4917 --- /dev/null +++ b/gateway-service-knoxidf/src/main/java/org/apache/knox/gateway/service/knoxidf/RegistrationResource.java @@ -0,0 +1,153 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package org.apache.knox.gateway.service.knoxidf; + +import com.nimbusds.jose.KeyLengthException; +import org.apache.commons.lang3.StringUtils; +import org.apache.knox.gateway.service.knoxtoken.ClientCredentialsResource; +import org.apache.knox.gateway.services.ServiceLifecycleException; +import org.apache.knox.gateway.services.security.AliasServiceException; +import org.apache.knox.gateway.services.security.token.TokenMetadata; +import org.glassfish.jersey.process.internal.RequestScoped; + +import javax.annotation.PostConstruct; +import javax.servlet.ServletException; +import javax.ws.rs.Consumes; +import javax.ws.rs.FormParam; +import javax.ws.rs.GET; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +import static org.apache.knox.gateway.util.knoxidf.KnoxIDFConstants.BASE_RESORCE_PATH; +import static org.apache.knox.gateway.util.knoxidf.KnoxIDFConstants.DEFAULT_SCOPES; +import static org.apache.knox.gateway.util.knoxidf.KnoxIDFUtils.error; + +@Path(RegistrationResource.RESOURCE_PATH) +@RequestScoped //this is important because redirectUris/allowedScopes are part of the state of this class +public class RegistrationResource extends ClientCredentialsResource { + + static final String RESOURCE_PATH = BASE_RESORCE_PATH + "/client"; + private List redirectUris; + private List allowedScopes; + + @PostConstruct + @Override + public void init() throws ServletException, AliasServiceException, ServiceLifecycleException, KeyLengthException { + super.init(); + } + + @Override + @GET + public Response doGet() { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + @POST + public Response doPost() { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Path("/register") + @POST + @Consumes(MediaType.APPLICATION_FORM_URLENCODED) + public Response registerClient(@FormParam("redirect_uris") String redirectUris, + @FormParam("allowed_scopes") String allowedScopes) { + this.redirectUris = Arrays.asList(redirectUris.split(",")); + final Response redirectUriVerificationResponse = verifyRedirectUris(); + if (redirectUriVerificationResponse != null) { + return redirectUriVerificationResponse; + } + + if (StringUtils.isBlank(allowedScopes)) { + this.allowedScopes = new ArrayList<>(DEFAULT_SCOPES); + } else { + this.allowedScopes = Arrays.asList(allowedScopes.split(",")); + if (!this.allowedScopes.contains("openid")) { + return error("invalid_request", "allowed_scopes must include 'openid'"); + } + } + return super.doPost(); + } + + private Response verifyRedirectUris() { + if (redirectUris == null || redirectUris.isEmpty()) { + return error("invalid_request", "redirect_uris must be provided"); + } + + for (String uriStr : redirectUris) { + URI uri; + try { + uri = new URI(uriStr); + } catch (URISyntaxException e) { + return error("invalid_request", "Invalid redirect URI: " + uriStr); + } + + // Scheme check + if (!"https".equalsIgnoreCase(uri.getScheme()) && !"http".equalsIgnoreCase(uri.getScheme())) { + return error("invalid_request", "Redirect URI must use HTTPS or HTTP as scheme: " + uriStr); + } + + // Host check (no wildcard allowed) + if (uri.getHost() == null || uri.getHost().contains("*")) { + return error("invalid_request", "Wildcard not allowed in host: " + uriStr); + } + + // Path wildcard check + String path = uri.getPath(); + if (path != null && path.contains("*") && !path.endsWith("*")) { + return error("invalid_request", "Wildcard '*' only allowed at end of path: " + uriStr); + } + + // Query/fragment check + if ((uri.getQuery() != null && uri.getQuery().contains("*")) || + (uri.getFragment() != null && uri.getFragment().contains("*"))) { + return error("invalid_request", "Wildcard '*' not allowed in query or fragment: " + uriStr); + } + } + return null; + } + + @Override + protected void addArbitraryTokenMetadata(TokenMetadata tokenMetadata) { + tokenMetadata.add("redirect_uris", getRedirectUris()); + tokenMetadata.add("allowed_scopes", getAllowedScopes().replaceAll(",", " ")); + super.addArbitraryTokenMetadata(tokenMetadata); + } + + @Override + protected void decorateResponseMap(Map responseMap) { + responseMap.put("redirect_uris", getRedirectUris()); + responseMap.put("allowed_scopes", getAllowedScopes()); + } + + private String getRedirectUris() { + return String.join(",", redirectUris); + } + + private String getAllowedScopes() { + return String.join(",", allowedScopes); + } +} diff --git a/gateway-service-knoxidf/src/main/java/org/apache/knox/gateway/service/knoxidf/TokenResource.java b/gateway-service-knoxidf/src/main/java/org/apache/knox/gateway/service/knoxidf/TokenResource.java new file mode 100644 index 0000000000..fbbffd2ff0 --- /dev/null +++ b/gateway-service-knoxidf/src/main/java/org/apache/knox/gateway/service/knoxidf/TokenResource.java @@ -0,0 +1,252 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package org.apache.knox.gateway.service.knoxidf; + +import com.nimbusds.jose.KeyLengthException; +import org.apache.commons.lang3.StringUtils; +import org.apache.knox.gateway.service.knoxtoken.PasscodeTokenResourceBase; +import org.apache.knox.gateway.services.GatewayServices; +import org.apache.knox.gateway.services.ServiceLifecycleException; +import org.apache.knox.gateway.services.ServiceType; +import org.apache.knox.gateway.services.knoxidf.federation.FederatedIdentity; +import org.apache.knox.gateway.services.knoxidf.federation.FederatedIdentityService; +import org.apache.knox.gateway.services.security.AliasServiceException; +import org.apache.knox.gateway.services.security.token.JWTokenAttributesBuilder; +import org.apache.knox.gateway.services.security.token.JWTokenAuthority; +import org.apache.knox.gateway.services.security.token.TokenMetadata; +import org.apache.knox.gateway.services.security.token.TokenServiceException; +import org.apache.knox.gateway.services.security.token.UnknownTokenException; +import org.apache.knox.gateway.services.security.token.impl.JWT; + +import javax.annotation.PostConstruct; +import javax.servlet.ServletContext; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import java.util.HashMap; +import java.util.Map; + +import static org.apache.knox.gateway.util.knoxidf.KnoxIDFConstants.BASE_RESORCE_PATH; +import static org.apache.knox.gateway.util.knoxidf.KnoxIDFUtils.error; + +@Path(TokenResource.RESOURCE_PATH) +@Produces(MediaType.APPLICATION_JSON) +public class TokenResource extends PasscodeTokenResourceBase { + static final String RESOURCE_PATH = BASE_RESORCE_PATH + "/token"; + private UserParamsProvider userParamsProvider; + + @Context + private HttpServletRequest request; + + @Context + private ServletContext servletContext; + + private FederatedIdentityService federatedIdentityService; + + @Override + public String getPrefix() { + return "knoxidf"; + } + + @PostConstruct + @Override + public void init() throws ServletException, AliasServiceException, ServiceLifecycleException, KeyLengthException { + super.init(); + this.servletContext = wrapContextForDefaultParams(this.servletContext); + this.userParamsProvider = new LdapUserParamsProvider(servletContext.getInitParameter("user.params.provider.ldap.url")); + final GatewayServices services = (GatewayServices) servletContext.getAttribute(GatewayServices.GATEWAY_SERVICES_ATTRIBUTE); + federatedIdentityService = services.getService(ServiceType.KNOXIDF_FEDERATED_IDENTITY_SERVICE); + } + + @Override + @POST + public Response doPost() { + final String code = request.getParameter("code"); + final String redirectUri = request.getParameter("redirect_uri"); + + final Response paramVerificationErrorResponse = verifyParams(code, redirectUri); + if (paramVerificationErrorResponse == null) { + try { + validateAuthCode(code, redirectUri); + return getAuthenticationToken(); + } catch (AuthTokenValidationError e) { + return error("Auth code validation error", e.getMessage()); + } finally { + try { + tokenStateService.revokeToken(code); + } catch (UnknownTokenException e) { + //NOP: this should have been handled by the above UnknownTokenException already + } + } + } + return paramVerificationErrorResponse; + } + + @Override + protected UserContext buildUserContext(HttpServletRequest request) { + try { + final String code = request.getParameter("code"); + final TokenMetadata tokenMetadata = tokenStateService.getTokenMetadata(code); + final String scope = tokenMetadata.getMetadata("scope"); + final Map userParams = userParamsProvider.getParamsFor(tokenMetadata.getUserName(), scope); + userParams.put("scope", scope); + return new UserContext(tokenMetadata.getUserName(), null, userParams); + } catch (UnknownTokenException e) { + //this should not happen as we have just validated the auth code + throw new RuntimeException(e); + } + } + + @Override + protected void addArbitraryTokenMetadata(TokenMetadata tokenMetadata) { + try { + super.addArbitraryTokenMetadata(tokenMetadata); + final String code = request.getParameter("code"); + final TokenMetadata authCodeTokenMetadata = tokenStateService.getTokenMetadata(code); + + //if the auth code token was a result of a federated OIDC call, we need to save the associated + //federated identity ID in thw JWT too (so that it can be looked up while fetching user info) + final String federatedIdentityId = authCodeTokenMetadata.getMetadata("federated_identity_id"); + if (StringUtils.isNotBlank(federatedIdentityId)) { + tokenMetadata.add("federated_identity_id", federatedIdentityId); + } + } catch (UnknownTokenException e) { + //this should not happen as we have just validated the auth code + throw new RuntimeException(e); + } + } + + @Override + protected ResponseMap buildResponseMap(JWT token, long expires) throws TokenServiceException { + final ResponseMap responseMap = super.buildResponseMap(token, expires); + responseMap.map.put("id_token", generateIdToken(token)); + return responseMap; + } + + private String generateIdToken(JWT accessToken) throws TokenServiceException { + try { + final String code = request.getParameter("code"); + final TokenMetadata tokenMetadata = tokenStateService.getTokenMetadata(code); + final boolean hasFederatedIdToken = StringUtils.isNotBlank(tokenMetadata.getMetadata("federated_identity_id")); + + if (hasFederatedIdToken) { + return generateFederatedIdToken(accessToken, tokenMetadata); + } else { + return generateLocalIdToken(accessToken, tokenMetadata); + } + } catch (UnknownTokenException e) { + return null; //should not happen + } + } + + private String generateLocalIdToken(JWT accessToken, TokenMetadata tokenMetadata) throws TokenServiceException { + final JWTokenAttributesBuilder idTokenAttributesBuilder = new JWTokenAttributesBuilder(); + idTokenAttributesBuilder + .setAlgorithm(accessToken.getSignatureAlgorithm().getName()) + .setUserName(accessToken.getSubject()) + .setIssueTime(System.currentTimeMillis()) + .setExpires(Long.parseLong(accessToken.getExpires())) + .setIssuer(accessToken.getIssuer()); + final String associatedClientId = tokenMetadata.getMetadata("client_id"); + idTokenAttributesBuilder.setAudiences(associatedClientId); + final String nonce = tokenMetadata.getMetadata("nonce"); + if (StringUtils.isNotBlank(nonce)) { + idTokenAttributesBuilder.setCustomAttributes(Map.of("nonce", nonce)); + } + + final JWTokenAuthority ts = getGatewayServices().getService(ServiceType.TOKEN_SERVICE); + final JWT idToken = ts.issueToken(idTokenAttributesBuilder.build()); + return idToken.toString(); + } + + private String generateFederatedIdToken(JWT accessToken, TokenMetadata tokenMetadata) throws TokenServiceException { + final String fedIdentityId = tokenMetadata.getMetadata("federated_identity_id"); + final FederatedIdentity federatedIdentity = federatedIdentityService + .findById(fedIdentityId) + .orElseThrow(() -> new TokenServiceException("Federated identity not found")); + + final JWTokenAttributesBuilder builder = new JWTokenAttributesBuilder(); + builder.setAlgorithm(accessToken.getSignatureAlgorithm().getName()) + .setUserName(federatedIdentity.getUserId()) + .setIssueTime(System.currentTimeMillis()) + .setExpires(Long.parseLong(accessToken.getExpires())) + .setIssuer(accessToken.getIssuer()) + .setAudiences(tokenMetadata.getMetadata("client_id")); + + final Map claims = new HashMap<>(federatedIdentity.getAttributes()); + claims.keySet().retainAll(AuthorizeResource.ALLOWED_CLAIMS); + String nonce = tokenMetadata.getMetadata("nonce"); + if (StringUtils.isNotBlank(nonce)) { + claims.put("nonce", nonce); + } + + // Optional: indicate source for auditing/logging + claims.put("federated_idp", federatedIdentity.getProvider()); + claims.put("federated_sub", federatedIdentity.getExternalSubject()); + claims.put("federated_iss", federatedIdentity.getExternalIssuer()); + + builder.setCustomAttributes(claims); + + JWTokenAuthority ts = getGatewayServices().getService(ServiceType.TOKEN_SERVICE); + return ts.issueToken(builder.build()).toString(); + } + + private Response verifyParams(String code, String redirectUri) { + if (code == null || code.isEmpty()) { + return error("invalid_request", "Missing code"); + } + + if (redirectUri == null || redirectUri.isEmpty()) { + return error("invalid_request", "Missing redirect_uri"); + } + + return null; + } + + private void validateAuthCode(String code, String redirectUri) throws AuthTokenValidationError { + try { + final TokenMetadata tokenMetadata = tokenStateService.getTokenMetadata(code); + final String associatedClientId = tokenMetadata.getMetadata("client_id"); + final String associateRedirectUri = tokenMetadata.getMetadata("redirect_uri"); + if (!tokenMetadata.isAuthCode()) { + throw new AuthTokenValidationError("Invalid auth_code: not an auth code token"); //this one or the previous one might be redundant + } else if (tokenStateService.getTokenExpiration(code) <= System.currentTimeMillis()) { + throw new AuthTokenValidationError("Invalid auth_code: expired"); + } else if (!associateRedirectUri.equals(redirectUri)) { + throw new AuthTokenValidationError("Invalid redirect_uri: " + redirectUri); + } else { + final String clientId = request.getParameter("client_id"); + if (!associatedClientId.equals(clientId)) { + throw new AuthTokenValidationError("Invalid client_id: " + clientId); + } + } + } catch (UnknownTokenException e) { + throw new AuthTokenValidationError("Unknown auth_code"); + } + } + + private static class AuthTokenValidationError extends Exception { + AuthTokenValidationError(String message) { + super(message); + } + } +} diff --git a/gateway-service-knoxidf/src/main/java/org/apache/knox/gateway/service/knoxidf/UserInfoResource.java b/gateway-service-knoxidf/src/main/java/org/apache/knox/gateway/service/knoxidf/UserInfoResource.java new file mode 100644 index 0000000000..378b878ea1 --- /dev/null +++ b/gateway-service-knoxidf/src/main/java/org/apache/knox/gateway/service/knoxidf/UserInfoResource.java @@ -0,0 +1,137 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package org.apache.knox.gateway.service.knoxidf; + + +import org.apache.commons.lang3.StringUtils; +import org.apache.knox.gateway.services.GatewayServices; +import org.apache.knox.gateway.services.ServiceType; +import org.apache.knox.gateway.services.knoxidf.federation.FederatedIdentity; +import org.apache.knox.gateway.services.knoxidf.federation.FederatedIdentityService; +import org.apache.knox.gateway.services.security.token.TokenMetadata; +import org.apache.knox.gateway.services.security.token.TokenServiceException; +import org.apache.knox.gateway.services.security.token.TokenStateService; +import org.apache.knox.gateway.services.security.token.UnknownTokenException; +import org.apache.knox.gateway.util.JsonUtils; + +import javax.annotation.PostConstruct; +import javax.servlet.ServletContext; +import javax.servlet.http.HttpServletRequest; +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import java.util.HashMap; +import java.util.Map; +import java.util.stream.Collectors; + +import static org.apache.knox.gateway.util.knoxidf.KnoxIDFConstants.BASE_RESORCE_PATH; +import static org.apache.knox.gateway.util.knoxidf.KnoxIDFUtils.error; + + +@Path(UserInfoResource.RESOURCE_PATH) +@Produces(MediaType.APPLICATION_JSON) +public class UserInfoResource { + + static final String RESOURCE_PATH = BASE_RESORCE_PATH + "/userinfo"; + private UserParamsProvider userParamsProvider; + + @Context + private ServletContext servletContext; + + @Context + private HttpServletRequest request; + + private FederatedIdentityService federatedIdentityService; + + @PostConstruct + public void init() { + this.userParamsProvider = new LdapUserParamsProvider(servletContext.getInitParameter("user.params.provider.ldap.url")); + final GatewayServices services = (GatewayServices) servletContext.getAttribute(GatewayServices.GATEWAY_SERVICES_ATTRIBUTE); + federatedIdentityService = services.getService(ServiceType.KNOXIDF_FEDERATED_IDENTITY_SERVICE); + } + + public Response doGet() { + try { + return getUserInfo(); + } catch (UnknownTokenException | TokenServiceException e) { + throw new RuntimeException(e); + } + } + + public Response doPost() { + throw new UnsupportedOperationException(); + } + + @GET + @Produces(MediaType.APPLICATION_JSON) + public Response getUserInfo() throws UnknownTokenException, TokenServiceException { + final String tokenId = request.getAttribute("X-Token-Id") == null ? null : request.getAttribute("X-Token-Id").toString(); + if (tokenId == null) { + return error("invalid_request", "Cannot find tokenId"); + } + + final String scope = request.getAttribute("X-Token-Scope") == null ? "" : request.getAttribute("X-Token-Scope").toString(); + final TokenMetadata tokenMetadata = getReadonlyTokenStateService().getTokenMetadata(tokenId); + final Map userInfo = new HashMap<>(); + + // Check if this token has a federated identity + final String federatedIdentityId = tokenMetadata.getMetadata("federated_identity_id"); + + if (StringUtils.isNotBlank(federatedIdentityId)) { + // Federated user + final FederatedIdentity federatedIdentity = federatedIdentityService + .findById(federatedIdentityId) + .orElseThrow(() -> new TokenServiceException("Federated identity not found")); + + // Include only allowed claims + Map claims = federatedIdentity.getAttributes().entrySet().stream() + .filter(e -> AuthorizeResource.ALLOWED_CLAIMS.contains(e.getKey())) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); + + // Mandatory claims for OIDC + claims.put("sub", federatedIdentity.getUserId()); // internal Knox subject + claims.put("idp", federatedIdentity.getProvider()); + + // Optional: federated info for auditing + claims.put("federated_sub", federatedIdentity.getExternalSubject()); + claims.put("federated_iss", federatedIdentity.getExternalIssuer()); + + // Add nonce if available + String nonce = tokenMetadata.getMetadata("nonce"); + if (StringUtils.isNotBlank(nonce)) { + claims.put("nonce", nonce); + } + + userInfo.putAll(claims); + } else { + // Local Knox user + userInfo.putAll(userParamsProvider.getParamsFor(tokenMetadata.getUserName(), scope)); + } + + return Response.ok(JsonUtils.renderAsJsonString(userInfo, true)).build(); + } + + private TokenStateService getReadonlyTokenStateService() { + GatewayServices services = (GatewayServices) servletContext.getAttribute(GatewayServices.GATEWAY_SERVICES_ATTRIBUTE); + return services.getService(ServiceType.TOKEN_STATE_SERVICE); + } + +} + diff --git a/gateway-service-knoxidf/src/main/java/org/apache/knox/gateway/service/knoxidf/UserParamsProvider.java b/gateway-service-knoxidf/src/main/java/org/apache/knox/gateway/service/knoxidf/UserParamsProvider.java new file mode 100644 index 0000000000..04085e48d8 --- /dev/null +++ b/gateway-service-knoxidf/src/main/java/org/apache/knox/gateway/service/knoxidf/UserParamsProvider.java @@ -0,0 +1,30 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package org.apache.knox.gateway.service.knoxidf; + +import java.util.Map; + +public interface UserParamsProvider { + + /** + * Fetches OIDC parameters for the given subject name. + * + * @param subjectName The user login/ID (e.g., "sam"). + * @return a map of OIDC parameters (e.g., email, name, roles) + */ + Map getParamsFor(String subjectName, String scope); +} diff --git a/gateway-service-knoxidf/src/main/java/org/apache/knox/gateway/service/knoxidf/deploy/KnoxIDFServiceDeploymentContributor.java b/gateway-service-knoxidf/src/main/java/org/apache/knox/gateway/service/knoxidf/deploy/KnoxIDFServiceDeploymentContributor.java new file mode 100644 index 0000000000..cfbb4647cb --- /dev/null +++ b/gateway-service-knoxidf/src/main/java/org/apache/knox/gateway/service/knoxidf/deploy/KnoxIDFServiceDeploymentContributor.java @@ -0,0 +1,42 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package org.apache.knox.gateway.service.knoxidf.deploy; + +import org.apache.knox.gateway.jersey.JerseyServiceDeploymentContributorBase; + +public class KnoxIDFServiceDeploymentContributor extends JerseyServiceDeploymentContributorBase { + + @Override + public String getRole() { + return "KNOXIDF"; + } + + @Override + public String getName() { + return "KnoxIdentityFederation"; + } + + @Override + protected String[] getPackages() { + return new String[] { "org.apache.knox.gateway.service.knoxidf" }; + } + + @Override + protected String[] getPatterns() { + return new String[] { "knoxidf/api/**?**" }; + } +} diff --git a/gateway-service-knoxidf/src/main/resources/META-INF/services/org.apache.knox.gateway.deploy.ServiceDeploymentContributor b/gateway-service-knoxidf/src/main/resources/META-INF/services/org.apache.knox.gateway.deploy.ServiceDeploymentContributor new file mode 100644 index 0000000000..49fc687f12 --- /dev/null +++ b/gateway-service-knoxidf/src/main/resources/META-INF/services/org.apache.knox.gateway.deploy.ServiceDeploymentContributor @@ -0,0 +1,18 @@ +########################################################################## +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +########################################################################## +org.apache.knox.gateway.service.knoxidf.deploy.KnoxIDFServiceDeploymentContributor diff --git a/gateway-service-knoxsso/src/main/java/org/apache/knox/gateway/service/knoxsso/WebSSOResource.java b/gateway-service-knoxsso/src/main/java/org/apache/knox/gateway/service/knoxsso/WebSSOResource.java index 16e6762403..7a1caa2972 100644 --- a/gateway-service-knoxsso/src/main/java/org/apache/knox/gateway/service/knoxsso/WebSSOResource.java +++ b/gateway-service-knoxsso/src/main/java/org/apache/knox/gateway/service/knoxsso/WebSSOResource.java @@ -17,35 +17,6 @@ */ package org.apache.knox.gateway.service.knoxsso; -import static javax.ws.rs.core.MediaType.APPLICATION_JSON; -import static javax.ws.rs.core.MediaType.APPLICATION_XML; -import static org.apache.knox.gateway.services.GatewayServices.GATEWAY_CLUSTER_ATTRIBUTE; - -import java.io.IOException; -import java.net.MalformedURLException; -import java.net.URI; -import java.net.URISyntaxException; -import java.security.Principal; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import java.util.Map; -import java.util.Map.Entry; - -import javax.annotation.PostConstruct; -import javax.servlet.ServletContext; -import javax.servlet.http.Cookie; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import javax.servlet.http.HttpSession; -import javax.ws.rs.GET; -import javax.ws.rs.POST; -import javax.ws.rs.Path; -import javax.ws.rs.Produces; -import javax.ws.rs.WebApplicationException; -import javax.ws.rs.core.Context; -import javax.ws.rs.core.Response; - import com.nimbusds.jose.JOSEObjectType; import org.apache.commons.lang3.StringUtils; import org.apache.knox.gateway.audit.log4j.audit.Log4jAuditor; @@ -70,6 +41,37 @@ import org.apache.knox.gateway.util.Tokens; import org.apache.knox.gateway.util.Urls; import org.apache.knox.gateway.util.WhitelistUtils; +import org.apache.knox.gateway.util.knoxidf.AuthorizeRequestMetadataStore; +import org.apache.knox.gateway.util.knoxidf.FederatedOpConfiguration; +import org.apache.knox.gateway.util.knoxidf.KnoxIDFUtils; + +import javax.annotation.PostConstruct; +import javax.servlet.ServletContext; +import javax.servlet.http.Cookie; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpSession; +import javax.ws.rs.GET; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.Response; +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URI; +import java.net.URISyntaxException; +import java.security.Principal; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; + +import static javax.ws.rs.core.MediaType.APPLICATION_JSON; +import static javax.ws.rs.core.MediaType.APPLICATION_XML; +import static org.apache.knox.gateway.services.GatewayServices.GATEWAY_CLUSTER_ATTRIBUTE; @Path( WebSSOResource.RESOURCE_PATH ) public class WebSSOResource { @@ -108,13 +110,14 @@ public class WebSSOResource { private String tokenType; private String whitelist; private String domainSuffix; - private List targetAudiences = new ArrayList<>(); + private final List targetAudiences = new ArrayList<>(); private boolean enableSession; private String signatureAlgorithm; private List ssoExpectedparams = new ArrayList<>(); private String clusterName; private String tokenIssuer; private TokenStateService tokenStateService; + private final AuthorizeRequestMetadataStore authorizeRequestMetadataStore = AuthorizeRequestMetadataStore.getInstance(120000L); private String sameSiteValue; @@ -226,6 +229,17 @@ private void handleCookieSetup() { tokenType = StringUtils.isBlank(configuredTokenType) ? JOSEObjectType.JWT.getType() : configuredTokenType; } + @Path("/federated/op") + @GET + public Response federatedOpLogin() { + final String loginSessionId = request.getParameter("fedOpSid"); + final String opName = request.getParameter("fedOpName"); + final Map federatedOpMap = authorizeRequestMetadataStore.getFederatedOpConfiguration(loginSessionId); + final FederatedOpConfiguration federatedOpConfiguration = federatedOpMap.get(opName); + final String federatedOpAuthRedirect = KnoxIDFUtils.buildFederatedOpAuthRedirect(federatedOpConfiguration, loginSessionId); + return Response.seeOther(java.net.URI.create(federatedOpAuthRedirect)).build(); + } + @GET @Produces({APPLICATION_JSON, APPLICATION_XML}) public Response doGet() { diff --git a/gateway-service-knoxtoken/src/main/java/org/apache/knox/gateway/service/knoxtoken/ClientCredentialsResource.java b/gateway-service-knoxtoken/src/main/java/org/apache/knox/gateway/service/knoxtoken/ClientCredentialsResource.java index d23c693ee5..4c17c02db9 100644 --- a/gateway-service-knoxtoken/src/main/java/org/apache/knox/gateway/service/knoxtoken/ClientCredentialsResource.java +++ b/gateway-service-knoxtoken/src/main/java/org/apache/knox/gateway/service/knoxtoken/ClientCredentialsResource.java @@ -33,6 +33,7 @@ import javax.ws.rs.Produces; import javax.ws.rs.core.Response; import java.util.HashMap; +import java.util.Map; import static javax.ws.rs.core.MediaType.APPLICATION_JSON; import static javax.ws.rs.core.MediaType.APPLICATION_XML; @@ -98,6 +99,7 @@ public Response getAuthenticationToken() { map.put(CLIENT_ID, tokenId); map.put(CLIENT_SECRET, passcode); addExpiryIfNotNever(map); + decorateResponseMap(map); String jsonResponse = JsonUtils.renderAsJsonString(map); return resp.responseBuilder.entity(jsonResponse).build(); } @@ -108,4 +110,8 @@ public Response getAuthenticationToken() { return resp.responseBuilder.build(); } } + + protected void decorateResponseMap(Map responseMap) { + //NOP + } } diff --git a/gateway-service-knoxtoken/src/main/java/org/apache/knox/gateway/service/knoxtoken/TokenResource.java b/gateway-service-knoxtoken/src/main/java/org/apache/knox/gateway/service/knoxtoken/TokenResource.java index 6fec10e0f2..f7dc60c9e5 100644 --- a/gateway-service-knoxtoken/src/main/java/org/apache/knox/gateway/service/knoxtoken/TokenResource.java +++ b/gateway-service-knoxtoken/src/main/java/org/apache/knox/gateway/service/knoxtoken/TokenResource.java @@ -115,7 +115,7 @@ public class TokenResource { protected static final String TOKEN_TYPE = "token_type"; protected static final String ACCESS_TOKEN = "access_token"; protected static final String TOKEN_ID = "token_id"; - static final String PASSCODE = "passcode"; + public static final String PASSCODE = "passcode"; protected static final String MANAGED_TOKEN = "managed"; private static final String TARGET_URL = "target_url"; private static final String ENDPOINT_PUBLIC_CERT = "endpoint_public_cert"; @@ -145,6 +145,7 @@ public class TokenResource { private static final String LIFESPAN_INPUT_ENABLED_TEXT = "lifespanInputEnabled"; static final String KNOX_TOKEN_USER_LIMIT_PER_USER = TOKEN_PARAM_PREFIX + "limit.per.user"; static final String KNOX_TOKEN_USER_LIMIT_EXCEEDED_ACTION = TOKEN_PARAM_PREFIX + "user.limit.exceeded.action"; + private static final String KNOX_TOKEN_HARDCODED_CLAIM_MAPPINGS = TOKEN_PARAM_PREFIX + "hardcoded.claim.mappings"; private static final String METADATA_QUERY_PARAM_PREFIX = "md_"; private static final long TOKEN_TTL_DEFAULT = 30000L; static final String TOKEN_API_PATH = "knoxtoken/api/v1"; @@ -186,6 +187,7 @@ public class TokenResource { private Optional maxTokenLifetime = Optional.empty(); private int tokenLimitPerUser; + private Map hardCodedClaimMappings; private boolean includeGroupsInTokenAllowed; private String tokenIssuer; @@ -359,9 +361,37 @@ public void init() throws AliasServiceException, ServiceLifecycleException, KeyL .filter(s -> !s.isEmpty()) .collect(Collectors.toSet()); } + + parseHardcodedClaimMappings(context.getInitParameter(KNOX_TOKEN_HARDCODED_CLAIM_MAPPINGS)); setTokenStateServiceStatusMap(); } + private void parseHardcodedClaimMappings(String raw) { + hardCodedClaimMappings = new HashMap<>(); + + if (raw != null && !raw.isBlank()) { + Arrays.stream(raw.split(";")) + .map(String::trim) + .filter(s -> !s.isEmpty()) + .map(entry -> entry.split("=", 2)) + .filter(kv -> kv.length == 2) + .forEach(kv -> { + String key = kv[0].trim(); + String value = kv[1].trim(); + + Object mappedValue = + value.contains(",") + ? Arrays.stream(value.split(",")) + .map(String::trim) + .filter(v -> !v.isEmpty()) + .toList() + : value; + + hardCodedClaimMappings.put(key, mappedValue); + }); + } + } + private String getTokenTTLAsText() { if (tokenTTL == -1) { return "Unlimited lifetime"; @@ -839,16 +869,17 @@ protected Response getAuthenticationToken() { protected TokenResponseContext getTokenResponse(UserContext context) { TokenResponseContext response = null; + long issueTime = System.currentTimeMillis(); long expires = getExpiry(); setupPublicCertPEM(); String jku = getJku(); try { - JWT token = getJWT(context.userName, expires, jku); + JWT token = getJWT(context, issueTime, expires, jku); if (token != null) { ResponseMap result = buildResponseMap(token, expires); String jsonResponse = JsonUtils.renderAsJsonString(result.map); - persistTokenDetails(result, expires, context.userName, context.createdBy); + persistTokenDetails(result, issueTime, expires, context.userName, context.createdBy); response = new TokenResponseContext(result, jsonResponse, Response.ok()); } else { @@ -934,10 +965,16 @@ protected UserContext buildUserContext(HttpServletRequest request) { protected static class UserContext { public final String userName; public final String createdBy; + private final Map userParams; public UserContext(String userName, String createdBy) { + this(userName, createdBy, Collections.emptyMap()); + } + + public UserContext(String userName, String createdBy, Map userParams) { this.userName = userName; this.createdBy = createdBy; + this.userParams = userParams; } } @@ -1009,13 +1046,10 @@ protected Response enforceClientCertIfRequired() { return response; } - protected void persistTokenDetails(ResponseMap result, long expires, String userName, String createdBy) { + protected void persistTokenDetails(ResponseMap result, long issueTime, long expires, String userName, String createdBy) { // Optional token store service persistence if (tokenStateService != null) { - final long issueTime = System.currentTimeMillis(); - tokenStateService.addToken(result.tokenId, - issueTime, - expires, + tokenStateService.addToken(result.tokenId, issueTime, expires, maxTokenLifetime.orElse(tokenStateService.getDefaultMaxLifetimeDuration())); final String comment = request.getParameter(COMMENT); final TokenMetadata tokenMetadata = new TokenMetadata(userName, StringUtils.isBlank(comment) ? null : comment); @@ -1029,7 +1063,7 @@ protected void persistTokenDetails(ResponseMap result, long expires, String user } } - protected ResponseMap buildResponseMap(JWT token, long expires) { + protected ResponseMap buildResponseMap(JWT token, long expires) throws TokenServiceException { String accessToken = token.toString(); String tokenId = TokenUtils.getTokenId(token); final boolean managedToken = tokenStateService != null; @@ -1073,7 +1107,7 @@ public ResponseMap(String accessToken, String tokenId, Map map, } } - protected JWT getJWT(String userName, long expires, String jku) throws TokenServiceException { + private JWT getJWT(UserContext userContext, long issueTime, long expires, String jku) throws TokenServiceException { JWTokenAttributes jwtAttributes; JWT token; JWTokenAuthority ts = getGatewayServices().getService(ServiceType.TOKEN_SERVICE); @@ -1081,8 +1115,9 @@ protected JWT getJWT(String userName, long expires, String jku) throws TokenServ final JWTokenAttributesBuilder jwtAttributesBuilder = new JWTokenAttributesBuilder(); jwtAttributesBuilder .setIssuer(tokenIssuer) - .setUserName(userName) + .setUserName(userContext.userName) .setAlgorithm(signatureAlgorithm) + .setIssueTime(issueTime) .setExpires(expires) .setManaged(managedToken) .setJku(jku) @@ -1102,6 +1137,14 @@ protected JWT getJWT(String userName, long expires, String jku) throws TokenServ } } + if (userContext.userParams != null) { + hardCodedClaimMappings.putAll(userContext.userParams); + } + + if (!hardCodedClaimMappings.isEmpty()) { + jwtAttributesBuilder.setCustomAttributes(hardCodedClaimMappings); + } + jwtAttributes = jwtAttributesBuilder.build(); token = ts.issueToken(jwtAttributes); return token; diff --git a/gateway-spi/src/main/java/org/apache/knox/gateway/security/CommonTokenConstants.java b/gateway-spi/src/main/java/org/apache/knox/gateway/security/CommonTokenConstants.java index 1537f698b2..2f28b96293 100644 --- a/gateway-spi/src/main/java/org/apache/knox/gateway/security/CommonTokenConstants.java +++ b/gateway-spi/src/main/java/org/apache/knox/gateway/security/CommonTokenConstants.java @@ -27,4 +27,6 @@ public interface CommonTokenConstants { String CLIENT_SECRET = "client_secret"; + String AUTH_CODE = "authorization_code"; + } diff --git a/gateway-spi/src/main/java/org/apache/knox/gateway/services/ServiceType.java b/gateway-spi/src/main/java/org/apache/knox/gateway/services/ServiceType.java index d96db8d495..5fbcba3ab5 100644 --- a/gateway-spi/src/main/java/org/apache/knox/gateway/services/ServiceType.java +++ b/gateway-spi/src/main/java/org/apache/knox/gateway/services/ServiceType.java @@ -39,7 +39,8 @@ public enum ServiceType { CONCURRENT_SESSION_VERIFIER("ConcurrentSessionVerifier"), REMOTE_CONFIGURATION_MONITOR("RemoteConfigurationMonitor"), GATEWAY_STATUS_SERVICE("GatewayStatusService"), - LDAP_SERVICE("LDAPService"); + LDAP_SERVICE("LDAPService"), + KNOXIDF_FEDERATED_IDENTITY_SERVICE("KnoxIDFFederatedIdentityService"); private final String serviceTypeName; private final String shortName; diff --git a/gateway-spi/src/main/java/org/apache/knox/gateway/services/knoxidf/federation/FederatedIdentity.java b/gateway-spi/src/main/java/org/apache/knox/gateway/services/knoxidf/federation/FederatedIdentity.java new file mode 100644 index 0000000000..3c025ccb82 --- /dev/null +++ b/gateway-spi/src/main/java/org/apache/knox/gateway/services/knoxidf/federation/FederatedIdentity.java @@ -0,0 +1,87 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package org.apache.knox.gateway.services.knoxidf.federation; + +import java.time.Instant; +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +public final class FederatedIdentity { + + private final String id; + private final String userId; + private final String provider; + private final String externalSubject; + private final String externalIssuer; + private final Instant createdAt; + private final Map attributes = new HashMap<>(); + + public FederatedIdentity(String userId, String provider, String externalSubject, String externalIssuer, + Instant createdAt, Map attributes) { + this(UUID.randomUUID().toString(), userId, provider, externalSubject, externalIssuer, createdAt, attributes); + } + + public FederatedIdentity(String id, String userId, String provider, String externalSubject, String externalIssuer, + Instant createdAt, Map attributes) { + this.id = id; + this.userId = userId; + this.provider = provider; + this.externalSubject = externalSubject; + this.externalIssuer = externalIssuer; + this.createdAt = createdAt; + if (attributes != null) { + this.attributes.putAll(attributes); + } + } + + public String getId() { + return id; + } + + public String getUserId() { + return userId; + } + + public String getProvider() { + return provider; + } + + public String getExternalSubject() { + return externalSubject; + } + + public String getExternalIssuer() { + return externalIssuer; + } + + public Instant getCreatedAt() { + return createdAt; + } + + public Map getAttributes() { + return attributes; + } + + public String getAttribute(String key) { + return attributes.get(key); + } + + public void addAttribute(String key, String value) { + attributes.put(key, value); + } +} diff --git a/gateway-spi/src/main/java/org/apache/knox/gateway/services/knoxidf/federation/FederatedIdentityService.java b/gateway-spi/src/main/java/org/apache/knox/gateway/services/knoxidf/federation/FederatedIdentityService.java new file mode 100644 index 0000000000..778b589a24 --- /dev/null +++ b/gateway-spi/src/main/java/org/apache/knox/gateway/services/knoxidf/federation/FederatedIdentityService.java @@ -0,0 +1,34 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package org.apache.knox.gateway.services.knoxidf.federation; + +import org.apache.knox.gateway.services.Service; + +import java.util.Optional; + +public interface FederatedIdentityService extends Service { + + void addFederatedIdentity(FederatedIdentity identity); + + Optional findById(String identityId); + + Optional findByProviderAndSubject( + String provider, + String externalIssuer, + String externalSubject); +} + diff --git a/gateway-spi/src/main/java/org/apache/knox/gateway/services/security/token/JWTokenAttributes.java b/gateway-spi/src/main/java/org/apache/knox/gateway/services/security/token/JWTokenAttributes.java index 2df7cf53b3..2b12bf55e3 100644 --- a/gateway-spi/src/main/java/org/apache/knox/gateway/services/security/token/JWTokenAttributes.java +++ b/gateway-spi/src/main/java/org/apache/knox/gateway/services/security/token/JWTokenAttributes.java @@ -19,120 +19,132 @@ import java.net.URI; import java.net.URISyntaxException; +import java.util.Collections; import java.util.Date; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.Set; public class JWTokenAttributes { - public static final String DEFAULT_ISSUER = "KNOXSSO"; - public static final String DEFAULT_TYPE = "JWT"; - private final String userName; - private final List audiences; - private final String algorithm; - private final long expires; - private final String signingKeystoreName; - private final String signingKeystoreAlias; - private final char[] signingKeystorePassphrase; - private final boolean managed; - private String jku; - private final String type; - private final Set groups; - private final String issuer; - private String kid; - private final String clientId; - - JWTokenAttributes(String userName, List audiences, String algorithm, long expires, String signingKeystoreName, String signingKeystoreAlias, - char[] signingKeystorePassphrase, boolean managed, String jku, String type, Set groups, String kid, String issuer) { - this(userName, audiences, algorithm, expires, signingKeystoreName, signingKeystoreAlias, signingKeystorePassphrase, managed, jku, type, groups, kid, issuer, null); - } - - JWTokenAttributes(String userName, List audiences, String algorithm, long expires, String signingKeystoreName, String signingKeystoreAlias, - char[] signingKeystorePassphrase, boolean managed, String jku, String type, Set groups, String kid, String issuer, String clientId) { - this.userName = userName; - this.audiences = audiences; - this.algorithm = algorithm; - this.expires = expires; - this.signingKeystoreName = signingKeystoreName; - this.signingKeystoreAlias = signingKeystoreAlias; - this.signingKeystorePassphrase = signingKeystorePassphrase; - this.managed = managed; - this.jku = jku; - this.type = type; - this.groups = groups; - this.kid = kid; - this.issuer = issuer; - this.clientId = clientId; - } - - public String getUserName() { - return userName; - } - - public List getAudiences() { - return audiences; - } - - public String getAlgorithm() { - return algorithm; - } - - public long getExpires() { - return expires; - } - - public Date getExpiresDate() { - return expires == -1 ? null : new Date(expires); - } - - public String getSigningKeystoreName() { - return signingKeystoreName; - } - - public String getSigningKeystoreAlias() { - return signingKeystoreAlias; - } - - public char[] getSigningKeystorePassphrase() { - return signingKeystorePassphrase; - } - - public boolean isManaged() { - return managed; - } - - public URI getJkuUri() throws URISyntaxException { - return jku != null ? new URI(jku) : null; - } - - public String getJku(){ - return jku; - } - - public void setJku(String jku) { - this.jku = jku; - } - - public String getType() { - return type; - } - - public Set getGroups() { - return groups; - } - - public void setKid(String kid) { - this.kid = kid; - } - - public String getKid() { - return kid; - } - - public String getIssuer() { - return issuer; - } - - public String getClientId() { - return clientId; - } + public static final String DEFAULT_ISSUER = "KNOXSSO"; + public static final String DEFAULT_TYPE = "JWT"; + private final String userName; + private final List audiences; + private final String algorithm; + private final long issueTime; + private final long expires; + private final String signingKeystoreName; + private final String signingKeystoreAlias; + private final char[] signingKeystorePassphrase; + private final boolean managed; + private String jku; + private final String type; + private final Set groups; + private final String issuer; + private String kid; + private final String clientId; + private final Map customAttributes; + + JWTokenAttributes(String userName, List audiences, String algorithm, long issueTime, long expires, String signingKeystoreName, String signingKeystoreAlias, + char[] signingKeystorePassphrase, boolean managed, String jku, String type, Set groups, String kid, String issuer, String clientId, + Map customAttributes) { + this.userName = userName; + this.audiences = audiences; + this.algorithm = algorithm; + this.issueTime = issueTime; + this.expires = expires; + this.signingKeystoreName = signingKeystoreName; + this.signingKeystoreAlias = signingKeystoreAlias; + this.signingKeystorePassphrase = signingKeystorePassphrase; + this.managed = managed; + this.jku = jku; + this.type = type; + this.groups = groups; + this.kid = kid; + this.issuer = issuer; + this.clientId = clientId; + this.customAttributes = customAttributes; + } + + public String getUserName() { + return userName; + } + + public List getAudiences() { + return audiences; + } + + public String getAlgorithm() { + return algorithm; + } + + public long getIssueTime() { + return issueTime; + } + + public long getExpires() { + return expires; + } + + public Date getExpiresDate() { + return expires == -1 ? null : new Date(expires); + } + + public String getSigningKeystoreName() { + return signingKeystoreName; + } + + public String getSigningKeystoreAlias() { + return signingKeystoreAlias; + } + + public char[] getSigningKeystorePassphrase() { + return signingKeystorePassphrase; + } + + public boolean isManaged() { + return managed; + } + + public URI getJkuUri() throws URISyntaxException { + return jku != null ? new URI(jku) : null; + } + + public String getJku() { + return jku; + } + + public void setJku(String jku) { + this.jku = jku; + } + + public String getType() { + return type; + } + + public Set getGroups() { + return groups; + } + + public void setKid(String kid) { + this.kid = kid; + } + + public String getKid() { + return kid; + } + + public String getIssuer() { + return issuer; + } + + + public String getClientId() { + return clientId; + } + + public Map getCustomAttributes() { + return customAttributes; + } } diff --git a/gateway-spi/src/main/java/org/apache/knox/gateway/services/security/token/JWTokenAttributesBuilder.java b/gateway-spi/src/main/java/org/apache/knox/gateway/services/security/token/JWTokenAttributesBuilder.java index 91f4acddf7..af60ae8901 100644 --- a/gateway-spi/src/main/java/org/apache/knox/gateway/services/security/token/JWTokenAttributesBuilder.java +++ b/gateway-spi/src/main/java/org/apache/knox/gateway/services/security/token/JWTokenAttributesBuilder.java @@ -20,6 +20,7 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.Map; import java.util.Set; public class JWTokenAttributesBuilder { @@ -27,6 +28,7 @@ public class JWTokenAttributesBuilder { private String userName; private List audiences; private String algorithm; + private long issueTime; private long expires; private String signingKeystoreName; private String signingKeystoreAlias; @@ -38,6 +40,7 @@ public class JWTokenAttributesBuilder { private String kid; private String issuer = JWTokenAttributes.DEFAULT_ISSUER; private String clientId; + private Map customAttributes; public JWTokenAttributesBuilder setUserName(String userName) { this.userName = userName; @@ -58,6 +61,11 @@ public JWTokenAttributesBuilder setAlgorithm(String algorithm) { return this; } + public JWTokenAttributesBuilder setIssueTime(long issueTime) { + this.issueTime = issueTime; + return this; + } + public JWTokenAttributesBuilder setExpires(long expires) { this.expires = expires; return this; @@ -113,8 +121,13 @@ public JWTokenAttributesBuilder setClientId(String clientId) { return this; } + public JWTokenAttributesBuilder setCustomAttributes(Map customAttributes) { + this.customAttributes = customAttributes; + return this; + } + public JWTokenAttributes build() { - return new JWTokenAttributes(userName, (audiences == null ? new ArrayList<>() : audiences), algorithm, expires, signingKeystoreName, signingKeystoreAlias, - signingKeystorePassphrase, managed, jku, type, groups, kid, issuer, clientId); + return new JWTokenAttributes(userName, (audiences == null ? new ArrayList<>() : audiences), algorithm, issueTime, expires, signingKeystoreName, signingKeystoreAlias, + signingKeystorePassphrase, managed, jku, type, groups, kid, issuer, clientId, customAttributes); } } diff --git a/gateway-spi/src/main/java/org/apache/knox/gateway/services/security/token/TokenMetadata.java b/gateway-spi/src/main/java/org/apache/knox/gateway/services/security/token/TokenMetadata.java index 3bc7fe2cda..8df35babe5 100644 --- a/gateway-spi/src/main/java/org/apache/knox/gateway/services/security/token/TokenMetadata.java +++ b/gateway-spi/src/main/java/org/apache/knox/gateway/services/security/token/TokenMetadata.java @@ -70,7 +70,6 @@ private void saveMetadata(String key, String value) { } public TokenMetadata(Map metadataMap) { - this.metadataMap.clear(); this.metadataMap.putAll(metadataMap); } @@ -151,12 +150,17 @@ public void markKnoxSsoCookie() { @JsonIgnore public boolean isKnoxSsoCookie() { - return getType() == null ? false : TokenMetadataType.KNOXSSO_COOKIE == TokenMetadataType.valueOf(getType()); + return getType() != null && TokenMetadataType.KNOXSSO_COOKIE == TokenMetadataType.valueOf(getType()); } @JsonIgnore public boolean isClientId() { - return getType() == null ? false : TokenMetadataType.CLIENT_ID == TokenMetadataType.valueOf(getType()); + return getType() != null && TokenMetadataType.CLIENT_ID == TokenMetadataType.valueOf(getType()); + } + + @JsonIgnore + public boolean isAuthCode() { + return getType() != null && TokenMetadataType.AUTH_CODE == TokenMetadataType.valueOf(getType()); } public String getType() { diff --git a/gateway-spi/src/main/java/org/apache/knox/gateway/services/security/token/TokenMetadataType.java b/gateway-spi/src/main/java/org/apache/knox/gateway/services/security/token/TokenMetadataType.java index 17e82e0af5..8a13bf1d48 100644 --- a/gateway-spi/src/main/java/org/apache/knox/gateway/services/security/token/TokenMetadataType.java +++ b/gateway-spi/src/main/java/org/apache/knox/gateway/services/security/token/TokenMetadataType.java @@ -18,6 +18,6 @@ public enum TokenMetadataType { - JWT, KNOXSSO_COOKIE, CLIENT_ID, API_KEY; + JWT, KNOXSSO_COOKIE, CLIENT_ID, API_KEY, AUTH_CODE; } diff --git a/gateway-spi/src/main/java/org/apache/knox/gateway/services/security/token/impl/JWT.java b/gateway-spi/src/main/java/org/apache/knox/gateway/services/security/token/impl/JWT.java index e416e63aaa..d4935c6e58 100644 --- a/gateway-spi/src/main/java/org/apache/knox/gateway/services/security/token/impl/JWT.java +++ b/gateway-spi/src/main/java/org/apache/knox/gateway/services/security/token/impl/JWT.java @@ -23,6 +23,7 @@ import com.nimbusds.jose.JWSAlgorithm; import com.nimbusds.jose.JWSSigner; import com.nimbusds.jose.JWSVerifier; +import com.nimbusds.jwt.JWTClaimsSet; public interface JWT { @@ -61,6 +62,8 @@ public interface JWT { String getClaims(); + JWTClaimsSet getJWTClaimsSet(); + JWSAlgorithm getSignatureAlgorithm(); JOSEObjectType getType(); diff --git a/gateway-spi/src/main/java/org/apache/knox/gateway/services/security/token/impl/JWTToken.java b/gateway-spi/src/main/java/org/apache/knox/gateway/services/security/token/impl/JWTToken.java index 22d8dc16ff..a0c01cd27c 100644 --- a/gateway-spi/src/main/java/org/apache/knox/gateway/services/security/token/impl/JWTToken.java +++ b/gateway-spi/src/main/java/org/apache/knox/gateway/services/security/token/impl/JWTToken.java @@ -18,6 +18,7 @@ import java.net.URISyntaxException; import java.text.ParseException; +import java.time.Instant; import java.util.Date; import java.util.UUID; @@ -81,6 +82,7 @@ public JWTToken(JWTokenAttributes jwtAttributes) { } JWTClaimsSet claims; JWTClaimsSet.Builder builder = new JWTClaimsSet.Builder() + .issueTime(Date.from(Instant.ofEpochMilli(jwtAttributes.getIssueTime()))) .issuer(jwtAttributes.getIssuer()) .subject(jwtAttributes.getUserName()) .audience(jwtAttributes.getAudiences()); @@ -104,6 +106,11 @@ public JWTToken(JWTokenAttributes jwtAttributes) { builder.claim(KNOX_ID_CLAIM, String.valueOf(UUID.randomUUID())); builder.claim(MANAGED_TOKEN_CLAIM, String.valueOf(jwtAttributes.isManaged())); + + if (jwtAttributes.getCustomAttributes() != null) { + jwtAttributes.getCustomAttributes().forEach(builder::claim); + } + claims = builder.build(); jwt = new SignedJWT(header, claims); @@ -138,6 +145,16 @@ public String getClaims() { return c; } + @Override + public JWTClaimsSet getJWTClaimsSet() { + try { + return jwt.getJWTClaimsSet(); + } catch (ParseException e) { + log.unableToParseToken(e); + return null; + } + } + @Override public String getPayload() { Payload payload = jwt.getPayload(); diff --git a/gateway-util-common/pom.xml b/gateway-util-common/pom.xml index 1eec58fe97..88d9c93114 100644 --- a/gateway-util-common/pom.xml +++ b/gateway-util-common/pom.xml @@ -104,6 +104,23 @@ org.apache.httpcomponents httpclient + + + javax.ws.rs + javax.ws.rs-api + + + org.apache.commons + commons-text + + + com.github.ben-manes.caffeine + caffeine + + + com.google.guava + guava + diff --git a/gateway-util-common/src/main/java/org/apache/knox/gateway/util/JsonUtils.java b/gateway-util-common/src/main/java/org/apache/knox/gateway/util/JsonUtils.java index f0a8bf177b..ab49f7d636 100644 --- a/gateway-util-common/src/main/java/org/apache/knox/gateway/util/JsonUtils.java +++ b/gateway-util-common/src/main/java/org/apache/knox/gateway/util/JsonUtils.java @@ -23,6 +23,7 @@ import java.util.HashMap; import java.util.Map; +import com.fasterxml.jackson.databind.ObjectWriter; import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; import org.apache.knox.gateway.i18n.GatewayUtilCommonMessages; import org.apache.knox.gateway.i18n.messages.MessagesFactory; @@ -37,12 +38,16 @@ public class JsonUtils { private static final GatewayUtilCommonMessages LOG = MessagesFactory.get( GatewayUtilCommonMessages.class ); public static String renderAsJsonString(Map map) { + return renderAsJsonString(map, false); + } + + public static String renderAsJsonString(Map map, boolean pretty) { String json = null; ObjectMapper mapper = new ObjectMapper().registerModule(new JavaTimeModule()); try { - // write JSON to a file - json = mapper.writeValueAsString(map); + final ObjectWriter writer = pretty ? mapper.writerWithDefaultPrettyPrinter() : mapper.writer(); + json = writer.writeValueAsString(map); } catch ( JsonProcessingException e ) { LOG.failedToSerializeMapToJSON( map, e ); } diff --git a/gateway-util-common/src/main/java/org/apache/knox/gateway/util/knoxidf/AuthorizeRequestMetadata.java b/gateway-util-common/src/main/java/org/apache/knox/gateway/util/knoxidf/AuthorizeRequestMetadata.java new file mode 100644 index 0000000000..9042b2ea97 --- /dev/null +++ b/gateway-util-common/src/main/java/org/apache/knox/gateway/util/knoxidf/AuthorizeRequestMetadata.java @@ -0,0 +1,108 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package org.apache.knox.gateway.util.knoxidf; + +import javax.ws.rs.core.Response; +import java.util.Set; + +import static org.apache.knox.gateway.util.knoxidf.KnoxIDFUtils.error; + +public final class AuthorizeRequestMetadata { + private final String clientId; + private final String subject; + private final String responseType; + private final String redirectUri; + private final Set requestedScopes; + private final String state; + private final String nonce; + + public AuthorizeRequestMetadata(String clientId, String subject, String responseType, String redirectUri, Set requestedScopes, String state, String nonce) { + this.clientId = clientId; + this.subject = subject; + this.responseType = responseType; + this.redirectUri = redirectUri; + this.requestedScopes = requestedScopes; + this.state = state; + this.nonce = nonce; + } + + public Response verify() { + if (responseType == null || responseType.isEmpty()) { + return error("invalid_request", "Missing response_type"); + } else { + if (!KnoxIDFConstants.ALLOWED_RESPONSE_TYPES.contains(responseType)) { + return error("unsupported_response_type", "Unsupported response_type"); + } + + boolean requiresNonce = responseType.contains("id_token"); + if (requiresNonce && (nonce == null || nonce.isEmpty())) { + return error("invalid_request", "Missing required parameter: nonce"); + } + } + + if (clientId == null || clientId.isEmpty()) { + return error("invalid_request", "Missing client_id"); + } + + // Verify redirect URI + if (redirectUri == null || redirectUri.isEmpty()) { + return error("invalid_request", "Missing redirect_uri"); + } + + // Verify scope(s) + if (requestedScopes == null || requestedScopes.isEmpty()) { + return error("invalid_scope", "Missing scopes"); + } else if (!requestedScopes.contains("openid")) { + return error("invalid_scope", "Missing required scope: openid"); + } + + return null; + } + + public String getClientId() { + return clientId; + } + + public String getSubject() { + return subject; + } + + public String getResponseType() { + return responseType; + } + + public String getRedirectUri() { + return redirectUri; + } + + public String getState() { + return state; + } + + public String getNonce() { + return nonce; + } + + public Set getRequestedScopes() { + return requestedScopes; + } + + public String getJoinedRequestedScopes() { + return String.join(" ", requestedScopes); + } + +} diff --git a/gateway-util-common/src/main/java/org/apache/knox/gateway/util/knoxidf/AuthorizeRequestMetadataStore.java b/gateway-util-common/src/main/java/org/apache/knox/gateway/util/knoxidf/AuthorizeRequestMetadataStore.java new file mode 100644 index 0000000000..e728940b24 --- /dev/null +++ b/gateway-util-common/src/main/java/org/apache/knox/gateway/util/knoxidf/AuthorizeRequestMetadataStore.java @@ -0,0 +1,58 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package org.apache.knox.gateway.util.knoxidf; + +import com.github.benmanes.caffeine.cache.Cache; +import com.github.benmanes.caffeine.cache.Caffeine; + +import java.util.Map; +import java.util.concurrent.TimeUnit; + +public class AuthorizeRequestMetadataStore { + + private static AuthorizeRequestMetadataStore instance; + private final Cache authorizeRequestMetadataCache; + private final Cache> federatedOpConfigurationCache; + + private AuthorizeRequestMetadataStore(final long ttl) { + this.authorizeRequestMetadataCache = Caffeine.newBuilder().expireAfterWrite(ttl * 2, TimeUnit.MILLISECONDS).build(); + this.federatedOpConfigurationCache = Caffeine.newBuilder().expireAfterWrite(ttl * 2, TimeUnit.MILLISECONDS).build(); + } + + public static synchronized AuthorizeRequestMetadataStore getInstance(final long ttl) { + if (instance == null) { + instance = new AuthorizeRequestMetadataStore(ttl); + } + return instance; + } + + public void storeRequestMetadata(String state, AuthorizeRequestMetadata requestMetadata) { + authorizeRequestMetadataCache.put(state, requestMetadata); + } + + public AuthorizeRequestMetadata getRequestMetadata(String state) { + return authorizeRequestMetadataCache.getIfPresent(state); + } + + public void storeFederatedOpConfiguration(String sid, Map federatedOpConfigurationMap) { + federatedOpConfigurationCache.put(sid, federatedOpConfigurationMap); + } + + public Map getFederatedOpConfiguration(String sid) { + return federatedOpConfigurationCache.getIfPresent(sid); + } +} diff --git a/gateway-util-common/src/main/java/org/apache/knox/gateway/util/knoxidf/FederatedOpConfiguration.java b/gateway-util-common/src/main/java/org/apache/knox/gateway/util/knoxidf/FederatedOpConfiguration.java new file mode 100644 index 0000000000..4fa1d9ede6 --- /dev/null +++ b/gateway-util-common/src/main/java/org/apache/knox/gateway/util/knoxidf/FederatedOpConfiguration.java @@ -0,0 +1,81 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package org.apache.knox.gateway.util.knoxidf; + +import javax.servlet.ServletContext; + +public class FederatedOpConfiguration { + private final boolean enabled; + private final String name; + private final String clientId; + private final String clientSecret; + private final String tokenEndpoint; + private final String authorizeEndpoint; + private final String userInfoEndpoint; + private final String discoveryEndpoint; + private final String authorizeCallback; + + public FederatedOpConfiguration(final ServletContext servletContext) { + enabled = Boolean.parseBoolean(servletContext.getInitParameter(KnoxIDFConstants.FEDERATED_OP_CONFIG_ENABLED)); + name = servletContext.getInitParameter(KnoxIDFConstants.FEDERATED_OP_CONFIG_NAME); + clientId = servletContext.getInitParameter(KnoxIDFConstants.FEDERATED_OP_CONFIG_CLIENT_ID); + clientSecret = servletContext.getInitParameter(KnoxIDFConstants.FEDERATED_OP_CONFIG_CLIENT_SECRET); + tokenEndpoint = servletContext.getInitParameter(KnoxIDFConstants.FEDERATED_OP_CONFIG_TOKEN_ENDPOINT); + authorizeEndpoint = servletContext.getInitParameter(KnoxIDFConstants.FEDERATED_OP_CONFIG_AUTH_ENDPOINT); + authorizeCallback = servletContext.getInitParameter(KnoxIDFConstants.FEDERATED_OP_CONFIG_AUTH_CALLBACK); + userInfoEndpoint = servletContext.getInitParameter(KnoxIDFConstants.FEDERATED_OP_CONFIG_USERINFO_ENDPOINT); + discoveryEndpoint = servletContext.getInitParameter(KnoxIDFConstants.FEDERATED_OP_CONFIG_DISCOVERY_ENDPOINT); + } + + public String getName() { + return name; + } + + public boolean isFederatedOpRedirectEnabled() { + return enabled; + } + + + public String getClientId() { + return clientId; + } + + public String getClientSecret() { + return clientSecret; + } + + String getAuthorizeEndpoint() { + return authorizeEndpoint; + } + + public String getAuthorizeCallback() { + return authorizeCallback; + } + + public String getTokenEndpoint() { + return tokenEndpoint; + } + + public String getUserInfoEndpoint() { + return userInfoEndpoint; + } + + public String getDiscoveryEndpoint() { + return discoveryEndpoint; + } + +} diff --git a/gateway-util-common/src/main/java/org/apache/knox/gateway/util/knoxidf/FederatedOpConfigurationFactory.java b/gateway-util-common/src/main/java/org/apache/knox/gateway/util/knoxidf/FederatedOpConfigurationFactory.java new file mode 100644 index 0000000000..77f0ac2ded --- /dev/null +++ b/gateway-util-common/src/main/java/org/apache/knox/gateway/util/knoxidf/FederatedOpConfigurationFactory.java @@ -0,0 +1,28 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package org.apache.knox.gateway.util.knoxidf; + +import javax.servlet.ServletContext; +import java.util.Collections; +import java.util.Map; + +public class FederatedOpConfigurationFactory { + + public static Map createFederatedOpConfiguration(final ServletContext servletContext) { + return Collections.emptyMap(); + } +} diff --git a/gateway-util-common/src/main/java/org/apache/knox/gateway/util/knoxidf/KnoxIDFConstants.java b/gateway-util-common/src/main/java/org/apache/knox/gateway/util/knoxidf/KnoxIDFConstants.java new file mode 100644 index 0000000000..d33724f563 --- /dev/null +++ b/gateway-util-common/src/main/java/org/apache/knox/gateway/util/knoxidf/KnoxIDFConstants.java @@ -0,0 +1,53 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package org.apache.knox.gateway.util.knoxidf; + +import com.google.common.collect.Sets; + +import java.util.Set; + +public interface KnoxIDFConstants { + String BASE_RESORCE_PATH = "knoxidf/api/v1"; + String GRANT_TYPE = "grant_type"; + String CLIENT_CREDENTIALS = "client_credentials"; + String AUTH_CODE = "authorization_code"; + String CLIENT_SECRET = "client_secret"; + String CLIENT_ID = "client_id"; + String REDIRECT_URI = "redirect_uri"; + String RESPONSE_TYPE = "response_type"; + Set ALLOWED_RESPONSE_TYPES = Sets.newHashSet("code", "id_token", "code id_token"); + String SCOPE = "scope"; + Set DEFAULT_SCOPES = Sets.newHashSet("openid", "profile", "email"); + String OPENID_SCOPE = SCOPE + "=openid"; + String STATE = "state"; + String CODE = "code"; + String CODE_RESPONSE_TYPE = RESPONSE_TYPE + "=" + CODE; + String NONCE = "nonce"; + + String FEDERATED_ID_TOKEN_PREFIX = "fed_id_"; + String FEDERATED_ACCESS_TOKEN_PREFIX = "fed_access_"; + String FEDERATED_OP_CONFIG_PREFIX = "federated.op."; + String FEDERATED_OP_CONFIG_ENABLED = FEDERATED_OP_CONFIG_PREFIX + "enabled"; + String FEDERATED_OP_CONFIG_NAME = FEDERATED_OP_CONFIG_PREFIX + "name"; + String FEDERATED_OP_CONFIG_CLIENT_ID = FEDERATED_OP_CONFIG_PREFIX + "clientId"; + String FEDERATED_OP_CONFIG_CLIENT_SECRET = FEDERATED_OP_CONFIG_PREFIX + "clientSecret"; + String FEDERATED_OP_CONFIG_TOKEN_ENDPOINT = FEDERATED_OP_CONFIG_PREFIX + "token.endpoint"; + String FEDERATED_OP_CONFIG_AUTH_ENDPOINT = FEDERATED_OP_CONFIG_PREFIX + "authorize.endpoint"; + String FEDERATED_OP_CONFIG_AUTH_CALLBACK = FEDERATED_OP_CONFIG_PREFIX + "authorize.callback"; + String FEDERATED_OP_CONFIG_USERINFO_ENDPOINT = FEDERATED_OP_CONFIG_PREFIX + "userinfo.endpoint"; + String FEDERATED_OP_CONFIG_DISCOVERY_ENDPOINT = FEDERATED_OP_CONFIG_PREFIX + "discovery.endpoint"; +} diff --git a/gateway-util-common/src/main/java/org/apache/knox/gateway/util/knoxidf/KnoxIDFUtils.java b/gateway-util-common/src/main/java/org/apache/knox/gateway/util/knoxidf/KnoxIDFUtils.java new file mode 100644 index 0000000000..3d3fdcd121 --- /dev/null +++ b/gateway-util-common/src/main/java/org/apache/knox/gateway/util/knoxidf/KnoxIDFUtils.java @@ -0,0 +1,109 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package org.apache.knox.gateway.util.knoxidf; + +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.text.StringEscapeUtils; +import org.apache.knox.gateway.util.JsonUtils; + +import javax.servlet.ServletContext; +import javax.servlet.http.HttpServletRequest; +import javax.ws.rs.core.Response; +import java.util.Arrays; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + + +public class KnoxIDFUtils { + + private static final int CHUNK_SIZE = 255; + + public static Map splitFederatedToken(String token, boolean idToken) { + final String prefix = idToken ? KnoxIDFConstants.FEDERATED_ID_TOKEN_PREFIX : KnoxIDFConstants.FEDERATED_ACCESS_TOKEN_PREFIX; + final Map parts = new LinkedHashMap<>(); + int i = 0, part = 1; + while (i < token.length()) { + int end = Math.min(i + CHUNK_SIZE, token.length()); + parts.put(prefix + part++, token.substring(i, end)); + i = end; + } + return parts; + } + + public static String joinFederatedToken(Map tokenMetadataMap, boolean idToken) { + final String prefix = idToken ? KnoxIDFConstants.FEDERATED_ID_TOKEN_PREFIX : KnoxIDFConstants.FEDERATED_ACCESS_TOKEN_PREFIX; + return tokenMetadataMap.entrySet().stream() + .filter(e -> e.getKey().startsWith(prefix)) + .sorted(Map.Entry.comparingByKey( + Comparator.comparingInt(k -> Integer.parseInt(k.replace(prefix, ""))) + )) + .map(Map.Entry::getValue) + .collect(Collectors.joining()); + } + + public static Response error(String error, String description) { + final Map errorMeap = new HashMap<>(); + errorMeap.put("error", error); + errorMeap.put("error_description", description); + return Response.status(Response.Status.UNAUTHORIZED).entity(JsonUtils.renderAsJsonString(errorMeap)).build(); + } + + public static String getRequestParamSafe(final HttpServletRequest request, final String key) { + String value = request.getParameter(key); + if (value == null) { + return ""; + } else { + return StringEscapeUtils.escapeHtml4(value); + } + } + + public static Set fetchEnabledFederatedOpConfigs(final HttpServletRequest request) { + final ServletContext servletContext = request.getServletContext(); + if (servletContext == null) { + return Collections.emptySet(); + } + final FederatedOpConfiguration federatedOpConfiguration = new FederatedOpConfiguration(servletContext); + return federatedOpConfiguration.isFederatedOpRedirectEnabled() ? Collections.singleton(federatedOpConfiguration) : Collections.emptySet(); + } + + public static AuthorizeRequestMetadata buildAuthRequestMetadata(final HttpServletRequest request) { + final String clientId = request.getParameter(KnoxIDFConstants.CLIENT_ID); + final String responseType = request.getParameter(KnoxIDFConstants.RESPONSE_TYPE); + final String redirectUri = request.getParameter(KnoxIDFConstants.REDIRECT_URI); + final String scope = request.getParameter(KnoxIDFConstants.SCOPE); + final Set requestedScopes = StringUtils.isBlank(scope) ? KnoxIDFConstants.DEFAULT_SCOPES : new HashSet<>(Arrays.asList(scope.split("\\s+"))); + final String state = request.getParameter(KnoxIDFConstants.STATE); + final String nonce = request.getParameter(KnoxIDFConstants.NONCE); + return new AuthorizeRequestMetadata(clientId, null, responseType, redirectUri, requestedScopes, state, nonce); + } + + public static String buildFederatedOpAuthRedirect(final FederatedOpConfiguration federatedOpConfiguration, final String federatedState) { + return federatedOpConfiguration.getAuthorizeEndpoint() + + "?" + KnoxIDFConstants.CLIENT_ID + "=" + federatedOpConfiguration.getClientId() + + "&" + KnoxIDFConstants.REDIRECT_URI + "=" + federatedOpConfiguration.getAuthorizeCallback() + + "&" + KnoxIDFConstants.CODE_RESPONSE_TYPE + + "&" + KnoxIDFConstants.OPENID_SCOPE + + "&" + KnoxIDFConstants.STATE + "=" + federatedState; + } + +} diff --git a/pom.xml b/pom.xml index a2626889a6..6bab9a87d3 100644 --- a/pom.xml +++ b/pom.xml @@ -153,6 +153,7 @@ knox-token-management-ui knox-webshell-ui knox-token-generation-ui + gateway-service-knoxidf @@ -291,6 +292,7 @@ 1.2.5 1.15.1 2.4.0-b180830.0438 + 5.2.0 2.4.1 6.4.0 4.0.4 @@ -1411,6 +1413,11 @@ knox-token-generation-ui ${project.version} + + org.apache.knox + gateway-service-knoxidf + ${project.version} + org.glassfish.jersey.containers jersey-container-servlet-core @@ -1426,6 +1433,11 @@ jersey-server ${jersey.version} + + org.glassfish.jersey.core + jersey-common + ${jersey.version} + org.glassfish.jersey.inject @@ -2021,6 +2033,11 @@ woodstox-core ${woodstox-core.version} + + com.fasterxml.uuid + java-uuid-generator + ${uuid.generator.version} + cglib From 168aa53e7479232cfe71a42e7886c4f4ddf00ea0 Mon Sep 17 00:00:00 2001 From: Sandor Molnar Date: Tue, 21 Apr 2026 09:54:56 +0200 Subject: [PATCH 04/14] KnoxIDF - multi OP support --- .../jwt/filter/SSOCookieFederationFilter.java | 9 +- .../gateway/deploy/DeploymentFactory.java | 2 - .../knoxidf/AuthSuccessRedirectServlet.java | 94 ------------------- .../service/knoxidf/AuthorizeResource.java | 32 ++++--- .../service/knoxidf/DiscoveryResource.java | 24 ++--- .../service/knoxsso/WebSSOResource.java | 9 +- .../knoxidf/AuthorizeRequestMetadata.java | 13 +++ .../AuthorizeRequestMetadataStore.java | 11 --- .../knoxidf/FederatedOpConfiguration.java | 23 +++-- .../FederatedOpConfigurationFactory.java | 18 +++- .../util/knoxidf/KnoxIDFConstants.java | 9 -- .../gateway/util/knoxidf/KnoxIDFUtils.java | 6 +- 12 files changed, 80 insertions(+), 170 deletions(-) delete mode 100644 gateway-service-knoxidf/src/main/java/org/apache/knox/gateway/service/knoxidf/AuthSuccessRedirectServlet.java diff --git a/gateway-provider-security-jwt/src/main/java/org/apache/knox/gateway/provider/federation/jwt/filter/SSOCookieFederationFilter.java b/gateway-provider-security-jwt/src/main/java/org/apache/knox/gateway/provider/federation/jwt/filter/SSOCookieFederationFilter.java index b42d50d435..f659b662c1 100644 --- a/gateway-provider-security-jwt/src/main/java/org/apache/knox/gateway/provider/federation/jwt/filter/SSOCookieFederationFilter.java +++ b/gateway-provider-security-jwt/src/main/java/org/apache/knox/gateway/provider/federation/jwt/filter/SSOCookieFederationFilter.java @@ -55,7 +55,6 @@ import java.util.ArrayList; import java.util.HashSet; import java.util.List; -import java.util.Map; import java.util.Set; import java.util.stream.Collectors; @@ -348,13 +347,11 @@ protected String constructLoginURL(HttpServletRequest request) throws Unsupporte final Set enabledFederatedOpConfigs = KnoxIDFUtils.fetchEnabledFederatedOpConfigs(request); if (!enabledFederatedOpConfigs.isEmpty()) { final String loginSessionId = request.getSession().getId(); - final Map federatedOpMap = enabledFederatedOpConfigs.stream(). - collect(Collectors.toMap(FederatedOpConfiguration::getName, config -> config)); - authorizeRequestMetadataStore.storeFederatedOpConfiguration(loginSessionId, federatedOpMap); authorizeRequestMetadataStore.storeRequestMetadata(loginSessionId, KnoxIDFUtils.buildAuthRequestMetadata(request)); + final List opNames = enabledFederatedOpConfigs.stream().map(FederatedOpConfiguration::getName).collect(Collectors.toList()); providerURL += delimiter - + "federatedOpLoginSession=" + URLEncoder.encode(loginSessionId, "UTF-8") - + "&federatedOpNames=" + URLEncoder.encode(String.join(",", federatedOpMap.keySet()), "UTF-8"); + + "federatedOpLoginSession=" + URLEncoder.encode(loginSessionId, StandardCharsets.UTF_8) + + "&federatedOpNames=" + URLEncoder.encode(String.join(",", opNames), StandardCharsets.UTF_8); delimiter = "&"; } diff --git a/gateway-server/src/main/java/org/apache/knox/gateway/deploy/DeploymentFactory.java b/gateway-server/src/main/java/org/apache/knox/gateway/deploy/DeploymentFactory.java index 242bfb4d52..564f793007 100644 --- a/gateway-server/src/main/java/org/apache/knox/gateway/deploy/DeploymentFactory.java +++ b/gateway-server/src/main/java/org/apache/knox/gateway/deploy/DeploymentFactory.java @@ -379,8 +379,6 @@ private static void initialize( final boolean hasKnoxIdf = services!= null && services.entrySet().stream().anyMatch( e -> e.getKey().equalsIgnoreCase("KNOXIDF") ); if (hasKnoxIdf) { - wad.createServlet().servletName("auth-success-redirect").servletClass("org.apache.knox.gateway.service.knoxidf.AuthSuccessRedirectServlet"); - wad.createServletMapping().servletName("auth-success-redirect").urlPattern("/authSuccess"); wad.createServlet().servletName("auth-consent-redirect").servletClass("org.apache.knox.gateway.service.knoxidf.AuthConsentServlet"); wad.createServletMapping().servletName("auth-consent-redirect").urlPattern("/authConsent"); } diff --git a/gateway-service-knoxidf/src/main/java/org/apache/knox/gateway/service/knoxidf/AuthSuccessRedirectServlet.java b/gateway-service-knoxidf/src/main/java/org/apache/knox/gateway/service/knoxidf/AuthSuccessRedirectServlet.java deleted file mode 100644 index fd9a96a2e4..0000000000 --- a/gateway-service-knoxidf/src/main/java/org/apache/knox/gateway/service/knoxidf/AuthSuccessRedirectServlet.java +++ /dev/null @@ -1,94 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with this - * work for additional information regarding copyright ownership. The ASF - * licenses this file to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - *

- * http://www.apache.org/licenses/LICENSE-2.0 - *

- * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ -package org.apache.knox.gateway.service.knoxidf; - - -import javax.servlet.http.HttpServlet; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import java.io.IOException; -import java.io.PrintWriter; - -import static org.apache.knox.gateway.util.knoxidf.KnoxIDFUtils.getRequestParamSafe; - -public class AuthSuccessRedirectServlet extends HttpServlet { - - @Override - protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException { - resp.setContentType("text/html;charset=UTF-8"); - - final String code = getRequestParamSafe(req, "code"); - final String state = getRequestParamSafe(req, "state"); - - try (PrintWriter out = resp.getWriter()) { - out.println(""); - out.println(""); - out.println(""); - out.println(" "); - out.println(" Authentication Success"); - out.println(" "); - out.println(""); - out.println(""); - out.println("

"); - out.println("

✅ Authentication Successful!

"); - out.println("

Your authorization code:

"); - out.println("
" + code + "

"); - out.println(" "); - out.println("
Copied to clipboard!
"); - if (state != null) { - out.println("
"); - out.println("

State:

"); - out.println("
" + state + "
"); - } - out.println("
"); - out.println(" "); - out.println(""); - out.println(""); - } - } - -} diff --git a/gateway-service-knoxidf/src/main/java/org/apache/knox/gateway/service/knoxidf/AuthorizeResource.java b/gateway-service-knoxidf/src/main/java/org/apache/knox/gateway/service/knoxidf/AuthorizeResource.java index 52197bba0d..cf98e938b0 100644 --- a/gateway-service-knoxidf/src/main/java/org/apache/knox/gateway/service/knoxidf/AuthorizeResource.java +++ b/gateway-service-knoxidf/src/main/java/org/apache/knox/gateway/service/knoxidf/AuthorizeResource.java @@ -46,6 +46,7 @@ import org.apache.knox.gateway.util.knoxidf.AuthorizeRequestMetadata; import org.apache.knox.gateway.util.knoxidf.AuthorizeRequestMetadataStore; import org.apache.knox.gateway.util.knoxidf.FederatedOpConfiguration; +import org.apache.knox.gateway.util.knoxidf.FederatedOpConfigurationFactory; import org.apache.knox.gateway.util.knoxidf.KnoxIDFUtils; import javax.annotation.PostConstruct; @@ -88,7 +89,7 @@ public class AuthorizeResource extends PasscodeTokenResourceBase { "given_name", "family_name", "name", "locale"); private static final String UTF_8 = StandardCharsets.UTF_8.name(); - private FederatedOpConfiguration federatedOpConfiguration; + private Map federatedOpConfigurations; private AuthorizeRequestMetadataStore authorizeRequestMetadataStore; @Context @@ -103,7 +104,7 @@ public class AuthorizeResource extends PasscodeTokenResourceBase { @Override public void init() throws ServletException, AliasServiceException, ServiceLifecycleException, KeyLengthException { super.init(); - this.federatedOpConfiguration = new FederatedOpConfiguration(servletContext); + this.federatedOpConfigurations = FederatedOpConfigurationFactory.createFederatedOpConfiguration(servletContext); this.authorizeRequestMetadataStore = AuthorizeRequestMetadataStore.getInstance(tokenTTL); final GatewayServices services = (GatewayServices) servletContext.getAttribute(GatewayServices.GATEWAY_SERVICES_ATTRIBUTE); federatedIdentityService = services.getService(ServiceType.KNOXIDF_FEDERATED_IDENTITY_SERVICE); @@ -186,8 +187,9 @@ public Response authCallback() throws Exception { final String federatedAuthCode = request.getParameter("code"); final String state = request.getParameter("state"); final AuthorizeRequestMetadata authorizeRequestMetadata = authorizeRequestMetadataStore.getRequestMetadata(state); - final Pair federatedTokens = exchangeFederatedAuthCodeToTokens(federatedAuthCode); - final FederatedIdentity federatedIdentity = resolveFederatedIdentity(federatedTokens.getLeft()); + final String opName = authorizeRequestMetadata.getSelectedFederatedOpName(); + final Pair federatedTokens = exchangeFederatedAuthCodeToTokens(federatedAuthCode, federatedOpConfigurations.get(opName)); + final FederatedIdentity federatedIdentity = resolveFederatedIdentity(federatedTokens.getLeft(), opName); return getAuthCodeFromKnox(authorizeRequestMetadata, Pair.of(federatedIdentity.getId(), federatedTokens.getRight())); } @@ -282,30 +284,30 @@ private boolean matchesRedirectUri(String requestedUri, Set registeredUr return false; } - private Pair exchangeFederatedAuthCodeToTokens(String federatedAuthCode) { + private Pair exchangeFederatedAuthCodeToTokens(String federatedAuthCode, FederatedOpConfiguration opConfig) { String federatedIdToken = null; String federatedAccessToken = null; - final Response federatedTokenExchangeResponse = fetchFederatedTokens(federatedAuthCode, federatedOpConfiguration.getAuthorizeCallback()); + final Response federatedTokenExchangeResponse = fetchFederatedTokens(federatedAuthCode, opConfig.getAuthorizeCallback(), opConfig); if (federatedTokenExchangeResponse.getStatus() == Response.Status.OK.getStatusCode()) { final Map federatedTokenExchangeResponseBodyMap = JsonUtils.getMapFromJsonString((String) federatedTokenExchangeResponse.getEntity()); federatedIdToken = federatedTokenExchangeResponseBodyMap.get("id_token"); - federatedAccessToken = federatedTokenExchangeResponseBodyMap.get("access_token"); + federatedAccessToken = federatedTokenExchangeResponseBodyMap.get("access_token"); return Pair.of(federatedIdToken, federatedAccessToken); } else { throw new RuntimeException("Error fetching Federated Tokens from Federated Auth Code: " + federatedTokenExchangeResponse.getEntity()); } } - private Response fetchFederatedTokens(final String code, final String redirectUri) { + private Response fetchFederatedTokens(final String code, final String redirectUri, FederatedOpConfiguration opConfig) { final List params = new ArrayList<>(); params.add(new BasicNameValuePair("code", code)); params.add(new BasicNameValuePair("redirect_uri", redirectUri)); params.add(new BasicNameValuePair("grant_type", "authorization_code")); - params.add(new BasicNameValuePair("client_id", federatedOpConfiguration.getClientId())); - params.add(new BasicNameValuePair("client_secret", federatedOpConfiguration.getClientSecret())); + params.add(new BasicNameValuePair("client_id", opConfig.getClientId())); + params.add(new BasicNameValuePair("client_secret", opConfig.getClientSecret())); try (CloseableHttpClient httpClient = HttpClients.createDefault()) { - HttpPost post = new HttpPost(federatedOpConfiguration.getTokenEndpoint()); + HttpPost post = new HttpPost(opConfig.getTokenEndpoint()); post.setHeader("Content-Type", "application/x-www-form-urlencoded"); post.setEntity(new UrlEncodedFormEntity(params, StandardCharsets.UTF_8)); @@ -319,14 +321,14 @@ private Response fetchFederatedTokens(final String code, final String redirectUr } } - private FederatedIdentity resolveFederatedIdentity(String federatedIdToken) throws ParseException { + private FederatedIdentity resolveFederatedIdentity(String federatedIdToken, String opName) throws ParseException { final JWT jwt = new JWTToken(federatedIdToken); final String issuer = jwt.getIssuer(); final String subject = jwt.getSubject(); - return federatedIdentityService.findByProviderAndSubject("KEYCLOAK", issuer, subject).orElseGet(() -> persistFederatedIdentity(jwt)); + return federatedIdentityService.findByProviderAndSubject(opName.toUpperCase(Locale.US), issuer, subject).orElseGet(() -> persistFederatedIdentity(jwt, opName)); } - private FederatedIdentity persistFederatedIdentity(final JWT jwt) { + private FederatedIdentity persistFederatedIdentity(final JWT jwt, String opName) { final Map attributes = jwt.getJWTClaimsSet().getClaims().entrySet().stream() .filter(e -> ALLOWED_CLAIMS.contains(e.getKey())) .filter(e -> e.getValue() != null) @@ -338,7 +340,7 @@ private FederatedIdentity persistFederatedIdentity(final JWT jwt) { )); final FederatedIdentity federatedIdentity = new FederatedIdentity( deriveKnoxSubject(jwt.getSubject(), jwt.getIssuer()), // internal user id (generated) - "KEYCLOAK", // provider + opName.toUpperCase(Locale.US), // provider jwt.getSubject(), // external subject jwt.getIssuer(), // external issuer Instant.now(), // createdAt diff --git a/gateway-service-knoxidf/src/main/java/org/apache/knox/gateway/service/knoxidf/DiscoveryResource.java b/gateway-service-knoxidf/src/main/java/org/apache/knox/gateway/service/knoxidf/DiscoveryResource.java index 84b6dccc6d..af357b7f78 100644 --- a/gateway-service-knoxidf/src/main/java/org/apache/knox/gateway/service/knoxidf/DiscoveryResource.java +++ b/gateway-service-knoxidf/src/main/java/org/apache/knox/gateway/service/knoxidf/DiscoveryResource.java @@ -16,8 +16,10 @@ */ package org.apache.knox.gateway.service.knoxidf; +import org.apache.knox.gateway.services.security.token.TokenUtils; import org.apache.knox.gateway.util.JsonUtils; import org.apache.knox.gateway.util.knoxidf.FederatedOpConfiguration; +import org.apache.knox.gateway.util.knoxidf.KnoxIDFConstants; import javax.annotation.PostConstruct; import javax.servlet.ServletContext; @@ -36,31 +38,21 @@ @Path(BASE_RESORCE_PATH + "/.well-known/openid-configuration") @Produces(MediaType.APPLICATION_JSON) public class DiscoveryResource { - private FederatedOpConfiguration federatedOpConfiguration; @Context private ServletContext servletContext; - @PostConstruct - public void init() { - this.federatedOpConfiguration = new FederatedOpConfiguration(servletContext); - } - @GET public Response getConfig(@Context UriInfo uriInfo) { final String baseUrl = uriInfo.getBaseUri().toString(); final Map config = new HashMap<>(); - config.put("issuer", baseUrl.replaceAll("awc-sso", "awc-token") + "knoxidf"); - config.put("authorization_endpoint", baseUrl .replaceAll("noauth", "sso")+ AuthorizeResource.RESOURCE_PATH); - config.put("token_endpoint", baseUrl - .replaceAll("awc-sso", "awc-token") - .replaceAll("noauth", "token") + TokenResource.RESOURCE_PATH); - config.put("userinfo_endpoint", baseUrl - .replaceAll("awc-sso", "awc-token") - .replaceAll("noauth", "token") + UserInfoResource.RESOURCE_PATH); + config.put("issuer", baseUrl + "knoxidf"); + config.put("authorization_endpoint", baseUrl + AuthorizeResource.RESOURCE_PATH); + config.put("token_endpoint", baseUrl + TokenResource.RESOURCE_PATH); + config.put("userinfo_endpoint", baseUrl + UserInfoResource.RESOURCE_PATH); config.put("jwks_uri", baseUrl + JwksResource.RESOURCE_PATH); - config.put("response_types_supported", new String[]{"code"}); - config.put("grant_types_supported", new String[]{"authorization_code"}); + config.put("response_types_supported", new String[]{KnoxIDFConstants.CODE}); + config.put("grant_types_supported", new String[]{KnoxIDFConstants.AUTH_CODE}); config.put("id_token_signing_alg_values_supported", new String[]{"RS256"}); return Response.ok(JsonUtils.renderAsJsonString(config)).build(); } diff --git a/gateway-service-knoxsso/src/main/java/org/apache/knox/gateway/service/knoxsso/WebSSOResource.java b/gateway-service-knoxsso/src/main/java/org/apache/knox/gateway/service/knoxsso/WebSSOResource.java index 7a1caa2972..b1e30b554f 100644 --- a/gateway-service-knoxsso/src/main/java/org/apache/knox/gateway/service/knoxsso/WebSSOResource.java +++ b/gateway-service-knoxsso/src/main/java/org/apache/knox/gateway/service/knoxsso/WebSSOResource.java @@ -41,8 +41,10 @@ import org.apache.knox.gateway.util.Tokens; import org.apache.knox.gateway.util.Urls; import org.apache.knox.gateway.util.WhitelistUtils; +import org.apache.knox.gateway.util.knoxidf.AuthorizeRequestMetadata; import org.apache.knox.gateway.util.knoxidf.AuthorizeRequestMetadataStore; import org.apache.knox.gateway.util.knoxidf.FederatedOpConfiguration; +import org.apache.knox.gateway.util.knoxidf.FederatedOpConfigurationFactory; import org.apache.knox.gateway.util.knoxidf.KnoxIDFUtils; import javax.annotation.PostConstruct; @@ -65,6 +67,7 @@ import java.security.Principal; import java.util.ArrayList; import java.util.Arrays; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; @@ -117,6 +120,7 @@ public class WebSSOResource { private String clusterName; private String tokenIssuer; private TokenStateService tokenStateService; + private final Map federatedOpConfigurations = null; private final AuthorizeRequestMetadataStore authorizeRequestMetadataStore = AuthorizeRequestMetadataStore.getInstance(120000L); private String sameSiteValue; @@ -234,8 +238,9 @@ private void handleCookieSetup() { public Response federatedOpLogin() { final String loginSessionId = request.getParameter("fedOpSid"); final String opName = request.getParameter("fedOpName"); - final Map federatedOpMap = authorizeRequestMetadataStore.getFederatedOpConfiguration(loginSessionId); - final FederatedOpConfiguration federatedOpConfiguration = federatedOpMap.get(opName); + final AuthorizeRequestMetadata metadata = authorizeRequestMetadataStore.getRequestMetadata(loginSessionId); + final FederatedOpConfiguration federatedOpConfiguration = FederatedOpConfigurationFactory.createFederatedOpConfiguration(context).get(opName); + metadata.setSelectedFederatedOpName(opName); final String federatedOpAuthRedirect = KnoxIDFUtils.buildFederatedOpAuthRedirect(federatedOpConfiguration, loginSessionId); return Response.seeOther(java.net.URI.create(federatedOpAuthRedirect)).build(); } diff --git a/gateway-util-common/src/main/java/org/apache/knox/gateway/util/knoxidf/AuthorizeRequestMetadata.java b/gateway-util-common/src/main/java/org/apache/knox/gateway/util/knoxidf/AuthorizeRequestMetadata.java index 9042b2ea97..a3dca7e41f 100644 --- a/gateway-util-common/src/main/java/org/apache/knox/gateway/util/knoxidf/AuthorizeRequestMetadata.java +++ b/gateway-util-common/src/main/java/org/apache/knox/gateway/util/knoxidf/AuthorizeRequestMetadata.java @@ -30,6 +30,8 @@ public final class AuthorizeRequestMetadata { private final String state; private final String nonce; + private String selectedFederatedOpName; + public AuthorizeRequestMetadata(String clientId, String subject, String responseType, String redirectUri, Set requestedScopes, String state, String nonce) { this.clientId = clientId; this.subject = subject; @@ -105,4 +107,15 @@ public String getJoinedRequestedScopes() { return String.join(" ", requestedScopes); } + public String getSelectedFederatedOpName() { + return selectedFederatedOpName; + } + + public void setSelectedFederatedOpName(String selectedFederatedOpName) { + this.selectedFederatedOpName = selectedFederatedOpName; + } + + public boolean hasSelectedFederatedOpName() { + return selectedFederatedOpName != null && !selectedFederatedOpName.isEmpty(); + } } diff --git a/gateway-util-common/src/main/java/org/apache/knox/gateway/util/knoxidf/AuthorizeRequestMetadataStore.java b/gateway-util-common/src/main/java/org/apache/knox/gateway/util/knoxidf/AuthorizeRequestMetadataStore.java index e728940b24..96311a6b48 100644 --- a/gateway-util-common/src/main/java/org/apache/knox/gateway/util/knoxidf/AuthorizeRequestMetadataStore.java +++ b/gateway-util-common/src/main/java/org/apache/knox/gateway/util/knoxidf/AuthorizeRequestMetadataStore.java @@ -19,18 +19,15 @@ import com.github.benmanes.caffeine.cache.Cache; import com.github.benmanes.caffeine.cache.Caffeine; -import java.util.Map; import java.util.concurrent.TimeUnit; public class AuthorizeRequestMetadataStore { private static AuthorizeRequestMetadataStore instance; private final Cache authorizeRequestMetadataCache; - private final Cache> federatedOpConfigurationCache; private AuthorizeRequestMetadataStore(final long ttl) { this.authorizeRequestMetadataCache = Caffeine.newBuilder().expireAfterWrite(ttl * 2, TimeUnit.MILLISECONDS).build(); - this.federatedOpConfigurationCache = Caffeine.newBuilder().expireAfterWrite(ttl * 2, TimeUnit.MILLISECONDS).build(); } public static synchronized AuthorizeRequestMetadataStore getInstance(final long ttl) { @@ -47,12 +44,4 @@ public void storeRequestMetadata(String state, AuthorizeRequestMetadata requestM public AuthorizeRequestMetadata getRequestMetadata(String state) { return authorizeRequestMetadataCache.getIfPresent(state); } - - public void storeFederatedOpConfiguration(String sid, Map federatedOpConfigurationMap) { - federatedOpConfigurationCache.put(sid, federatedOpConfigurationMap); - } - - public Map getFederatedOpConfiguration(String sid) { - return federatedOpConfigurationCache.getIfPresent(sid); - } } diff --git a/gateway-util-common/src/main/java/org/apache/knox/gateway/util/knoxidf/FederatedOpConfiguration.java b/gateway-util-common/src/main/java/org/apache/knox/gateway/util/knoxidf/FederatedOpConfiguration.java index 4fa1d9ede6..c385ad5ed7 100644 --- a/gateway-util-common/src/main/java/org/apache/knox/gateway/util/knoxidf/FederatedOpConfiguration.java +++ b/gateway-util-common/src/main/java/org/apache/knox/gateway/util/knoxidf/FederatedOpConfiguration.java @@ -30,15 +30,20 @@ public class FederatedOpConfiguration { private final String authorizeCallback; public FederatedOpConfiguration(final ServletContext servletContext) { - enabled = Boolean.parseBoolean(servletContext.getInitParameter(KnoxIDFConstants.FEDERATED_OP_CONFIG_ENABLED)); - name = servletContext.getInitParameter(KnoxIDFConstants.FEDERATED_OP_CONFIG_NAME); - clientId = servletContext.getInitParameter(KnoxIDFConstants.FEDERATED_OP_CONFIG_CLIENT_ID); - clientSecret = servletContext.getInitParameter(KnoxIDFConstants.FEDERATED_OP_CONFIG_CLIENT_SECRET); - tokenEndpoint = servletContext.getInitParameter(KnoxIDFConstants.FEDERATED_OP_CONFIG_TOKEN_ENDPOINT); - authorizeEndpoint = servletContext.getInitParameter(KnoxIDFConstants.FEDERATED_OP_CONFIG_AUTH_ENDPOINT); - authorizeCallback = servletContext.getInitParameter(KnoxIDFConstants.FEDERATED_OP_CONFIG_AUTH_CALLBACK); - userInfoEndpoint = servletContext.getInitParameter(KnoxIDFConstants.FEDERATED_OP_CONFIG_USERINFO_ENDPOINT); - discoveryEndpoint = servletContext.getInitParameter(KnoxIDFConstants.FEDERATED_OP_CONFIG_DISCOVERY_ENDPOINT); + this(servletContext, servletContext.getInitParameter(KnoxIDFConstants.FEDERATED_OP_CONFIG_NAME)); + } + + public FederatedOpConfiguration(final ServletContext servletContext, final String opName) { + this.enabled = Boolean.parseBoolean(servletContext.getInitParameter(KnoxIDFConstants.FEDERATED_OP_CONFIG_ENABLED)); + this.name = opName; + final String prefix = KnoxIDFConstants.FEDERATED_OP_CONFIG_PREFIX + (opName != null ? opName + "." : ""); + this.clientId = servletContext.getInitParameter(prefix + "clientId"); + this.clientSecret = servletContext.getInitParameter(prefix + "clientSecret"); + this.tokenEndpoint = servletContext.getInitParameter(prefix + "token.endpoint"); + this.authorizeEndpoint = servletContext.getInitParameter(prefix + "authorize.endpoint"); + this.authorizeCallback = servletContext.getInitParameter(prefix + "authorize.callback"); + this.userInfoEndpoint = servletContext.getInitParameter(prefix + "userinfo.endpoint"); + this.discoveryEndpoint = servletContext.getInitParameter(prefix + "discovery.endpoint"); } public String getName() { diff --git a/gateway-util-common/src/main/java/org/apache/knox/gateway/util/knoxidf/FederatedOpConfigurationFactory.java b/gateway-util-common/src/main/java/org/apache/knox/gateway/util/knoxidf/FederatedOpConfigurationFactory.java index 77f0ac2ded..b4fc5d4e5d 100644 --- a/gateway-util-common/src/main/java/org/apache/knox/gateway/util/knoxidf/FederatedOpConfigurationFactory.java +++ b/gateway-util-common/src/main/java/org/apache/knox/gateway/util/knoxidf/FederatedOpConfigurationFactory.java @@ -18,11 +18,27 @@ import javax.servlet.ServletContext; import java.util.Collections; +import java.util.HashMap; import java.util.Map; public class FederatedOpConfigurationFactory { public static Map createFederatedOpConfiguration(final ServletContext servletContext) { - return Collections.emptyMap(); + final String enabled = servletContext.getInitParameter(KnoxIDFConstants.FEDERATED_OP_CONFIG_ENABLED); + if (!Boolean.parseBoolean(enabled)) { + return Collections.emptyMap(); + } + + final String names = servletContext.getInitParameter(KnoxIDFConstants.FEDERATED_OP_CONFIG_NAME); + if (names == null || names.isEmpty()) { + return Collections.emptyMap(); + } + + final Map configs = new HashMap<>(); + for (String name : names.split(",")) { + String trimmedName = name.trim(); + configs.put(trimmedName, new FederatedOpConfiguration(servletContext, trimmedName)); + } + return configs; } } diff --git a/gateway-util-common/src/main/java/org/apache/knox/gateway/util/knoxidf/KnoxIDFConstants.java b/gateway-util-common/src/main/java/org/apache/knox/gateway/util/knoxidf/KnoxIDFConstants.java index d33724f563..bc8db9e48a 100644 --- a/gateway-util-common/src/main/java/org/apache/knox/gateway/util/knoxidf/KnoxIDFConstants.java +++ b/gateway-util-common/src/main/java/org/apache/knox/gateway/util/knoxidf/KnoxIDFConstants.java @@ -22,8 +22,6 @@ public interface KnoxIDFConstants { String BASE_RESORCE_PATH = "knoxidf/api/v1"; - String GRANT_TYPE = "grant_type"; - String CLIENT_CREDENTIALS = "client_credentials"; String AUTH_CODE = "authorization_code"; String CLIENT_SECRET = "client_secret"; String CLIENT_ID = "client_id"; @@ -43,11 +41,4 @@ public interface KnoxIDFConstants { String FEDERATED_OP_CONFIG_PREFIX = "federated.op."; String FEDERATED_OP_CONFIG_ENABLED = FEDERATED_OP_CONFIG_PREFIX + "enabled"; String FEDERATED_OP_CONFIG_NAME = FEDERATED_OP_CONFIG_PREFIX + "name"; - String FEDERATED_OP_CONFIG_CLIENT_ID = FEDERATED_OP_CONFIG_PREFIX + "clientId"; - String FEDERATED_OP_CONFIG_CLIENT_SECRET = FEDERATED_OP_CONFIG_PREFIX + "clientSecret"; - String FEDERATED_OP_CONFIG_TOKEN_ENDPOINT = FEDERATED_OP_CONFIG_PREFIX + "token.endpoint"; - String FEDERATED_OP_CONFIG_AUTH_ENDPOINT = FEDERATED_OP_CONFIG_PREFIX + "authorize.endpoint"; - String FEDERATED_OP_CONFIG_AUTH_CALLBACK = FEDERATED_OP_CONFIG_PREFIX + "authorize.callback"; - String FEDERATED_OP_CONFIG_USERINFO_ENDPOINT = FEDERATED_OP_CONFIG_PREFIX + "userinfo.endpoint"; - String FEDERATED_OP_CONFIG_DISCOVERY_ENDPOINT = FEDERATED_OP_CONFIG_PREFIX + "discovery.endpoint"; } diff --git a/gateway-util-common/src/main/java/org/apache/knox/gateway/util/knoxidf/KnoxIDFUtils.java b/gateway-util-common/src/main/java/org/apache/knox/gateway/util/knoxidf/KnoxIDFUtils.java index 3d3fdcd121..ebcec5a016 100644 --- a/gateway-util-common/src/main/java/org/apache/knox/gateway/util/knoxidf/KnoxIDFUtils.java +++ b/gateway-util-common/src/main/java/org/apache/knox/gateway/util/knoxidf/KnoxIDFUtils.java @@ -79,11 +79,7 @@ public static String getRequestParamSafe(final HttpServletRequest request, final public static Set fetchEnabledFederatedOpConfigs(final HttpServletRequest request) { final ServletContext servletContext = request.getServletContext(); - if (servletContext == null) { - return Collections.emptySet(); - } - final FederatedOpConfiguration federatedOpConfiguration = new FederatedOpConfiguration(servletContext); - return federatedOpConfiguration.isFederatedOpRedirectEnabled() ? Collections.singleton(federatedOpConfiguration) : Collections.emptySet(); + return servletContext == null ? Collections.emptySet() : new HashSet<>(FederatedOpConfigurationFactory.createFederatedOpConfiguration(servletContext).values()); } public static AuthorizeRequestMetadata buildAuthRequestMetadata(final HttpServletRequest request) { From 0bf7797cf5b02e003a85e831f697e53b5fd79d9f Mon Sep 17 00:00:00 2001 From: Sandor Molnar Date: Wed, 22 Apr 2026 11:01:46 +0200 Subject: [PATCH 05/14] KnoxIDF - make token endpoint configurable during discovery --- .../service/knoxidf/DiscoveryResource.java | 21 +++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/gateway-service-knoxidf/src/main/java/org/apache/knox/gateway/service/knoxidf/DiscoveryResource.java b/gateway-service-knoxidf/src/main/java/org/apache/knox/gateway/service/knoxidf/DiscoveryResource.java index af357b7f78..603c8a25c6 100644 --- a/gateway-service-knoxidf/src/main/java/org/apache/knox/gateway/service/knoxidf/DiscoveryResource.java +++ b/gateway-service-knoxidf/src/main/java/org/apache/knox/gateway/service/knoxidf/DiscoveryResource.java @@ -16,9 +16,8 @@ */ package org.apache.knox.gateway.service.knoxidf; -import org.apache.knox.gateway.services.security.token.TokenUtils; +import org.apache.knox.gateway.services.GatewayServices; import org.apache.knox.gateway.util.JsonUtils; -import org.apache.knox.gateway.util.knoxidf.FederatedOpConfiguration; import org.apache.knox.gateway.util.knoxidf.KnoxIDFConstants; import javax.annotation.PostConstruct; @@ -38,18 +37,32 @@ @Path(BASE_RESORCE_PATH + "/.well-known/openid-configuration") @Produces(MediaType.APPLICATION_JSON) public class DiscoveryResource { + private String currentTopologyName; + private String tokenExchangeTopologyName; @Context private ServletContext servletContext; + @PostConstruct + public void init() { + tokenExchangeTopologyName = servletContext.getInitParameter("token.exchange.topology.name"); + currentTopologyName = (String) servletContext.getAttribute(GatewayServices.GATEWAY_CLUSTER_ATTRIBUTE); + } + @GET public Response getConfig(@Context UriInfo uriInfo) { final String baseUrl = uriInfo.getBaseUri().toString(); final Map config = new HashMap<>(); config.put("issuer", baseUrl + "knoxidf"); config.put("authorization_endpoint", baseUrl + AuthorizeResource.RESOURCE_PATH); - config.put("token_endpoint", baseUrl + TokenResource.RESOURCE_PATH); - config.put("userinfo_endpoint", baseUrl + UserInfoResource.RESOURCE_PATH); + String tokenEndpoint = baseUrl + TokenResource.RESOURCE_PATH; + String userInfoEndpoint = baseUrl + UserInfoResource.RESOURCE_PATH; + if (tokenExchangeTopologyName != null) { + tokenEndpoint = tokenEndpoint.replaceAll(currentTopologyName, tokenExchangeTopologyName); + userInfoEndpoint = userInfoEndpoint.replaceAll(currentTopologyName, tokenExchangeTopologyName); + } + config.put("token_endpoint", tokenEndpoint); + config.put("userinfo_endpoint", userInfoEndpoint); config.put("jwks_uri", baseUrl + JwksResource.RESOURCE_PATH); config.put("response_types_supported", new String[]{KnoxIDFConstants.CODE}); config.put("grant_types_supported", new String[]{KnoxIDFConstants.AUTH_CODE}); From 52aca74379d4140543cd5ac0bf542558350f8efc Mon Sep 17 00:00:00 2001 From: Sandor Molnar Date: Thu, 23 Apr 2026 15:10:51 +0200 Subject: [PATCH 06/14] KnoxIDF - Code cleanup and bug fixes --- .../jwt/filter/SSOCookieFederationFilter.java | 5 +- .../database/AbstractDataSourceFactory.java | 12 +- .../knox/gateway/database/KnoxDatabase.java | 36 ++++ .../FederatedIdentityServiceFactory.java | 12 +- .../federation/FederatedIdentityDatabase.java | 132 ++++++++++++ .../FederatedIdentityServiceMessages.java | 35 ++++ .../JdbcFederatedIdentityService.java | 189 ++++-------------- .../token/impl/TokenStateDatabase.java | 14 +- ...oxIDFFederatedIdentityAttributesTable.sql} | 0 ...FederatedIdentityAttributesTableDerby.sql} | 0 ...ederatedIdentityAttributesTableOracle.sql} | 0 ...> createKnoxIDFFederatedIdentityTable.sql} | 0 ...ateKnoxIDFFederatedIdentityTableDerby.sql} | 0 ...teKnoxIDFFederatedIdentityTableOracle.sql} | 0 .../service/knoxidf/AuthorizeResource.java | 24 +-- .../service/knoxsso/WebSSOResource.java | 27 ++- .../FederatedIdentityServiceException.java | 28 +++ .../knoxidf/AuthorizeRequestMetadata.java | 13 -- .../AuthorizeRequestMetadataStore.java | 22 +- .../FederatedOpConfigurationStore.java | 35 ++++ .../util/knoxidf/KnoxIDFArtifactStore.java | 39 ++++ .../util/knoxidf/KnoxIDFConstants.java | 2 +- 22 files changed, 397 insertions(+), 228 deletions(-) create mode 100644 gateway-server/src/main/java/org/apache/knox/gateway/database/KnoxDatabase.java create mode 100644 gateway-server/src/main/java/org/apache/knox/gateway/services/knoxidf/federation/FederatedIdentityDatabase.java create mode 100644 gateway-server/src/main/java/org/apache/knox/gateway/services/knoxidf/federation/FederatedIdentityServiceMessages.java rename gateway-server/src/main/resources/{createKnoxcloakFederatedIdentityAttributesTable.sql => createKnoxIDFFederatedIdentityAttributesTable.sql} (100%) rename gateway-server/src/main/resources/{createKnoxcloakFederatedIdentityAttributesTableDerby.sql => createKnoxIDFFederatedIdentityAttributesTableDerby.sql} (100%) rename gateway-server/src/main/resources/{createKnoxcloakFederatedIdentityAttributesTableOracle.sql => createKnoxIDFFederatedIdentityAttributesTableOracle.sql} (100%) rename gateway-server/src/main/resources/{createKnoxcloakFederatedIdentityTable.sql => createKnoxIDFFederatedIdentityTable.sql} (100%) rename gateway-server/src/main/resources/{createKnoxcloakFederatedIdentityTableDerby.sql => createKnoxIDFFederatedIdentityTableDerby.sql} (100%) rename gateway-server/src/main/resources/{createKnoxcloakFederatedIdentityTableOracle.sql => createKnoxIDFFederatedIdentityTableOracle.sql} (100%) create mode 100644 gateway-spi/src/main/java/org/apache/knox/gateway/services/knoxidf/federation/FederatedIdentityServiceException.java create mode 100644 gateway-util-common/src/main/java/org/apache/knox/gateway/util/knoxidf/FederatedOpConfigurationStore.java create mode 100644 gateway-util-common/src/main/java/org/apache/knox/gateway/util/knoxidf/KnoxIDFArtifactStore.java diff --git a/gateway-provider-security-jwt/src/main/java/org/apache/knox/gateway/provider/federation/jwt/filter/SSOCookieFederationFilter.java b/gateway-provider-security-jwt/src/main/java/org/apache/knox/gateway/provider/federation/jwt/filter/SSOCookieFederationFilter.java index f659b662c1..8609f76b81 100644 --- a/gateway-provider-security-jwt/src/main/java/org/apache/knox/gateway/provider/federation/jwt/filter/SSOCookieFederationFilter.java +++ b/gateway-provider-security-jwt/src/main/java/org/apache/knox/gateway/provider/federation/jwt/filter/SSOCookieFederationFilter.java @@ -33,6 +33,7 @@ import org.apache.knox.gateway.util.Urls; import org.apache.knox.gateway.util.knoxidf.AuthorizeRequestMetadataStore; import org.apache.knox.gateway.util.knoxidf.FederatedOpConfiguration; +import org.apache.knox.gateway.util.knoxidf.FederatedOpConfigurationStore; import org.apache.knox.gateway.util.knoxidf.KnoxIDFUtils; import org.eclipse.jetty.http.MimeTypes; @@ -110,6 +111,7 @@ public class SSOCookieFederationFilter extends AbstractJWTFilter { private boolean verifyOriginalUrlFromHeaderDomain = DEFAULT_VERIFY_ORIGINAL_URL_FROM_HEADER_DOMAIN; private final List verifyOriginalUrlFromHeaderDomainWhitelist = new ArrayList<>(); private final AuthorizeRequestMetadataStore authorizeRequestMetadataStore = AuthorizeRequestMetadataStore.getInstance(120000L); + private final FederatedOpConfigurationStore federatedOpConfigurationStore = FederatedOpConfigurationStore.getInstance(120000L); private String originalUrlHeaderName; @Override @@ -347,7 +349,8 @@ protected String constructLoginURL(HttpServletRequest request) throws Unsupporte final Set enabledFederatedOpConfigs = KnoxIDFUtils.fetchEnabledFederatedOpConfigs(request); if (!enabledFederatedOpConfigs.isEmpty()) { final String loginSessionId = request.getSession().getId(); - authorizeRequestMetadataStore.storeRequestMetadata(loginSessionId, KnoxIDFUtils.buildAuthRequestMetadata(request)); + authorizeRequestMetadataStore.put(loginSessionId, KnoxIDFUtils.buildAuthRequestMetadata(request)); + federatedOpConfigurationStore.put(loginSessionId, enabledFederatedOpConfigs); final List opNames = enabledFederatedOpConfigs.stream().map(FederatedOpConfiguration::getName).collect(Collectors.toList()); providerURL += delimiter + "federatedOpLoginSession=" + URLEncoder.encode(loginSessionId, StandardCharsets.UTF_8) diff --git a/gateway-server/src/main/java/org/apache/knox/gateway/database/AbstractDataSourceFactory.java b/gateway-server/src/main/java/org/apache/knox/gateway/database/AbstractDataSourceFactory.java index 6cfefc7dd4..a9d544fe85 100644 --- a/gateway-server/src/main/java/org/apache/knox/gateway/database/AbstractDataSourceFactory.java +++ b/gateway-server/src/main/java/org/apache/knox/gateway/database/AbstractDataSourceFactory.java @@ -43,12 +43,12 @@ public abstract class AbstractDataSourceFactory { public static final String DERBY_KNOX_DESCRIPTORS_TABLE_CREATE_SQL_FILE_NAME = "createKnoxDescriptorsTableDerby.sql"; //KNOXIDF - public static final String KNOXIDF_FED_IDENTITY_TABLE_CREATE_SQL_FILE_NAME = "createKnoxcloakFederatedIdentityTable.sql"; - public static final String KNOXIDF_FED_IDENTITY_ATTR_TABLE_CREATE_SQL_FILE_NAME = "createKnoxcloakFederatedIdentityAttributesTable.sql"; - public static final String ORACLE_KNOXIDF_FED_IDENTITY_TABLE_CREATE_SQL_FILE_NAME = "createKnoxcloakFederatedIdentityTableOracle.sql"; - public static final String ORACLE_KNOXIDF_FED_IDENTITY_ATTR_TABLE_CREATE_SQL_FILE_NAME = "createKnoxcloakFederatedIdentityAttributesTableOracle.sql"; - public static final String DERBY_KNOXIDF_FED_IDENTITY_TABLE_CREATE_SQL_FILE_NAME = "createKnoxcloakFederatedIdentityTableDerby.sql"; - public static final String DERBY_KNOXIDF_FED_IDENTITY_ATTR_TABLE_CREATE_SQL_FILE_NAME = "createKnoxcloakFederatedIdentityAttributesTableDerby.sql"; + public static final String KNOXIDF_FED_IDENTITY_TABLE_CREATE_SQL_FILE_NAME = "createKnoxIDFFederatedIdentityTable.sql"; + public static final String KNOXIDF_FED_IDENTITY_ATTR_TABLE_CREATE_SQL_FILE_NAME = "createKnoxIDFFederatedIdentityAttributesTable.sql"; + public static final String ORACLE_KNOXIDF_FED_IDENTITY_TABLE_CREATE_SQL_FILE_NAME = "createKnoxIDFFederatedIdentityTableOracle.sql"; + public static final String ORACLE_KNOXIDF_FED_IDENTITY_ATTR_TABLE_CREATE_SQL_FILE_NAME = "createKnoxIDFFederatedIdentityAttributesTableOracle.sql"; + public static final String DERBY_KNOXIDF_FED_IDENTITY_TABLE_CREATE_SQL_FILE_NAME = "createKnoxIDFFederatedIdentityTableDerby.sql"; + public static final String DERBY_KNOXIDF_FED_IDENTITY_ATTR_TABLE_CREATE_SQL_FILE_NAME = "createKnoxIDFFederatedIdentityAttributesTableDerby.sql"; public static final String DATABASE_USER_ALIAS_NAME = "gateway_database_user"; public static final String DATABASE_PASSWORD_ALIAS_NAME = "gateway_database_password"; diff --git a/gateway-server/src/main/java/org/apache/knox/gateway/database/KnoxDatabase.java b/gateway-server/src/main/java/org/apache/knox/gateway/database/KnoxDatabase.java new file mode 100644 index 0000000000..261b368998 --- /dev/null +++ b/gateway-server/src/main/java/org/apache/knox/gateway/database/KnoxDatabase.java @@ -0,0 +1,36 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package org.apache.knox.gateway.database; + +import org.apache.knox.gateway.services.token.impl.TokenStateDatabase; + +import javax.sql.DataSource; + +public class KnoxDatabase { + + protected final DataSource dataSource; + + public KnoxDatabase(DataSource dataSource) { + this.dataSource = dataSource; + } + + protected void createTableIfNotExists(String tableName, String createSqlFileName) throws Exception { + if (!JDBCUtils.tableExists(tableName, dataSource)) { + JDBCUtils.createTableFromSQL(createSqlFileName, dataSource, TokenStateDatabase.class.getClassLoader()); + } + } +} diff --git a/gateway-server/src/main/java/org/apache/knox/gateway/services/factory/FederatedIdentityServiceFactory.java b/gateway-server/src/main/java/org/apache/knox/gateway/services/factory/FederatedIdentityServiceFactory.java index f38648fd66..e0107b672d 100644 --- a/gateway-server/src/main/java/org/apache/knox/gateway/services/factory/FederatedIdentityServiceFactory.java +++ b/gateway-server/src/main/java/org/apache/knox/gateway/services/factory/FederatedIdentityServiceFactory.java @@ -16,7 +16,9 @@ */ package org.apache.knox.gateway.services.factory; +import org.apache.knox.gateway.GatewayMessages; import org.apache.knox.gateway.config.GatewayConfig; +import org.apache.knox.gateway.i18n.messages.MessagesFactory; import org.apache.knox.gateway.services.GatewayServices; import org.apache.knox.gateway.services.Service; import org.apache.knox.gateway.services.ServiceLifecycleException; @@ -31,6 +33,7 @@ public class FederatedIdentityServiceFactory extends AbstractServiceFactory { + private static final GatewayMessages LOG = MessagesFactory.get(GatewayMessages.class); private static final String DEFAULT_IMPLEMENTATION = EmptyFederatedIdentitityService.class.getName(); @Override @@ -42,7 +45,14 @@ protected Service createService(GatewayServices gatewayServices, ServiceType ser service = new EmptyFederatedIdentitityService(); } else if (matchesImplementation(implementation, JdbcFederatedIdentityService.class)) { try { - service = new JdbcFederatedIdentityService(gatewayConfig, getAliasService(gatewayServices)); + try { + service = new JdbcFederatedIdentityService(); + ((JdbcFederatedIdentityService) service).setAliasService(getAliasService(gatewayServices)); + service.init(gatewayConfig, options); + } catch (ServiceLifecycleException e) { + LOG.errorInitializingService(implementation, e.getMessage(), e); + service = new EmptyFederatedIdentitityService(); + } } catch (Exception e) { throw new ServiceLifecycleException("Error while creating Federated Identity Service: " + e, e); } diff --git a/gateway-server/src/main/java/org/apache/knox/gateway/services/knoxidf/federation/FederatedIdentityDatabase.java b/gateway-server/src/main/java/org/apache/knox/gateway/services/knoxidf/federation/FederatedIdentityDatabase.java new file mode 100644 index 0000000000..649bf30388 --- /dev/null +++ b/gateway-server/src/main/java/org/apache/knox/gateway/services/knoxidf/federation/FederatedIdentityDatabase.java @@ -0,0 +1,132 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package org.apache.knox.gateway.services.knoxidf.federation; + +import org.apache.knox.gateway.database.DatabaseType; +import org.apache.knox.gateway.database.KnoxDatabase; + +import javax.sql.DataSource; +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Timestamp; +import java.util.HashMap; +import java.util.Optional; + +class FederatedIdentityDatabase extends KnoxDatabase { + private static final String FEDERATED_IDENTITY_TABLE_NAME = "federated_identity"; + private static final String FEDERATED_IDENTITY_ATTRIBUTES_TABLE_NAME = "federated_identity_attr"; + private static final String ADD_FEDERATED_IDENTITY_SQL = "INSERT INTO " + FEDERATED_IDENTITY_TABLE_NAME + + " (id, user_id, provider, external_subject, external_issuer, created_at) VALUES (?, ?, ?, ?, ?, ?)"; + private static final String ADD_FEDERATED_IDENTITY_ATTR_SQL = "INSERT INTO " + FEDERATED_IDENTITY_ATTRIBUTES_TABLE_NAME + + " (identity_id, attr_key, attr_value) VALUES (?, ?, ?)"; + private static final String FETCH_FEDERATED_IDENTITY_BY_PROV_ISS_SUB_SQL = "SELECT * FROM " + FEDERATED_IDENTITY_TABLE_NAME + + " WHERE provider = ? AND external_issuer = ? AND external_subject = ?"; + private static final String FETCH_FEDERATED_IDENTITY_SQL_BY_ID = "SELECT id, user_id, provider, external_subject, external_issuer, created_at FROM " + + FEDERATED_IDENTITY_TABLE_NAME + " WHERE id = ?"; + private static final String FETCH_FEDERATED_IDENTITY_ATTR_SQL = "SELECT attr_key, attr_value FROM " + FEDERATED_IDENTITY_ATTRIBUTES_TABLE_NAME + " WHERE identity_id = ?"; + + FederatedIdentityDatabase(DataSource dataSource, String dbType) throws Exception { + super(dataSource); + DatabaseType databaseType = DatabaseType.fromString(dbType); + createTableIfNotExists(FEDERATED_IDENTITY_TABLE_NAME, databaseType.federatedIdentityTableSql()); + createTableIfNotExists(FEDERATED_IDENTITY_ATTRIBUTES_TABLE_NAME, databaseType.federatedIdentityAttrTableSql()); + } + + void addFederatedIdentity(FederatedIdentity identity) throws SQLException { + // save core metadata first + try (Connection connection = dataSource.getConnection(); PreparedStatement addFederatedIdentityStatement = connection.prepareStatement(ADD_FEDERATED_IDENTITY_SQL)) { + addFederatedIdentityStatement.setString(1, identity.getId()); + addFederatedIdentityStatement.setString(2, identity.getUserId()); + addFederatedIdentityStatement.setString(3, identity.getProvider()); + addFederatedIdentityStatement.setString(4, identity.getExternalSubject()); + addFederatedIdentityStatement.setString(5, identity.getExternalIssuer()); + addFederatedIdentityStatement.setTimestamp(6, Timestamp.from(identity.getCreatedAt())); + addFederatedIdentityStatement.executeUpdate(); + } + + // save attributes + try (Connection connection = dataSource.getConnection(); PreparedStatement addFederatedIdentityAttrStatement = connection.prepareStatement(ADD_FEDERATED_IDENTITY_ATTR_SQL)) { + for (var attribute : identity.getAttributes().entrySet()) { + addFederatedIdentityAttrStatement.setString(1, identity.getId()); + addFederatedIdentityAttrStatement.setString(2, attribute.getKey()); + addFederatedIdentityAttrStatement.setString(3, attribute.getValue()); + addFederatedIdentityAttrStatement.addBatch(); + } + addFederatedIdentityAttrStatement.executeBatch(); + } + } + + + Optional findByProviderAndSubject(String provider, String issuer, String subject) throws SQLException { + FederatedIdentity federatedIdentity = null; + try (Connection connection = dataSource.getConnection(); PreparedStatement getFederatedIdentityStatement = connection.prepareStatement(FETCH_FEDERATED_IDENTITY_BY_PROV_ISS_SUB_SQL)) { + getFederatedIdentityStatement.setString(1, provider); + getFederatedIdentityStatement.setString(2, issuer); + getFederatedIdentityStatement.setString(3, subject); + try (ResultSet rs = getFederatedIdentityStatement.executeQuery()) { + if (rs.next()) { + federatedIdentity = new FederatedIdentity( + rs.getString("id"), + rs.getString("user_id"), + provider, + subject, + issuer, + rs.getTimestamp("created_at").toInstant(), new HashMap<>()); + } else { + return Optional.empty(); + } + } + } + populateAttributes(federatedIdentity); + return Optional.of(federatedIdentity); + } + + Optional findById(String id) throws SQLException { + FederatedIdentity federatedIdentity = null; + try (Connection connection = dataSource.getConnection(); PreparedStatement getFederatedIdentityStatement = connection.prepareStatement(FETCH_FEDERATED_IDENTITY_SQL_BY_ID)) { + getFederatedIdentityStatement.setString(1, id); + try (ResultSet rs = getFederatedIdentityStatement.executeQuery()) { + if (rs.next()) { + federatedIdentity = new FederatedIdentity( + id, + rs.getString("user_id"), + rs.getString("provider"), + rs.getString("external_subject"), + rs.getString("external_issuer"), + rs.getTimestamp("created_at").toInstant(), new HashMap<>()); + } else { + return Optional.empty(); + } + } + } + populateAttributes(federatedIdentity); + return Optional.of(federatedIdentity); + } + + private void populateAttributes(FederatedIdentity federatedIdentity) throws SQLException { + try (Connection connection = dataSource.getConnection(); PreparedStatement getFederatedIdentityAttrStatement = connection.prepareStatement(FETCH_FEDERATED_IDENTITY_ATTR_SQL)) { + getFederatedIdentityAttrStatement.setString(1, federatedIdentity.getId()); + try (ResultSet rs = getFederatedIdentityAttrStatement.executeQuery()) { + while (rs.next()) { + federatedIdentity.getAttributes().put(rs.getString(1), rs.getString(2)); + } + } + } + } +} diff --git a/gateway-server/src/main/java/org/apache/knox/gateway/services/knoxidf/federation/FederatedIdentityServiceMessages.java b/gateway-server/src/main/java/org/apache/knox/gateway/services/knoxidf/federation/FederatedIdentityServiceMessages.java new file mode 100644 index 0000000000..d6f820a1d5 --- /dev/null +++ b/gateway-server/src/main/java/org/apache/knox/gateway/services/knoxidf/federation/FederatedIdentityServiceMessages.java @@ -0,0 +1,35 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package org.apache.knox.gateway.services.knoxidf.federation; + +import org.apache.knox.gateway.i18n.messages.Message; +import org.apache.knox.gateway.i18n.messages.MessageLevel; +import org.apache.knox.gateway.i18n.messages.Messages; +import org.apache.knox.gateway.i18n.messages.StackTrace; + +@Messages(logger="org.apache.knox.gateway.knoxidf.federated.identity.service") +public interface FederatedIdentityServiceMessages { + + @Message(level = MessageLevel.ERROR, text = "An error occurred while saving federated identity {0} in the database : {1}") + void errorSavingFederatedIdentityInDatabase(String federatedIdentityId, String errorMessage, @StackTrace(level = MessageLevel.DEBUG) Exception e); + + @Message(level = MessageLevel.ERROR, text = "An error occurred while fetching federated identity ({0} / {1} / {2}) from the database : {3}") + void errorFetchingFederatedIdentityFromDatabase(String provider, String issuer, String subject, String errorMessage, @StackTrace(level = MessageLevel.DEBUG) Exception e); + + @Message(level = MessageLevel.ERROR, text = "An error occurred while fetching federated identity ({0}) from the database : {1}") + void errorFetchingFederatedIdentityFromDatabase(String id, String errorMessage, @StackTrace(level = MessageLevel.DEBUG) Exception e); +} diff --git a/gateway-server/src/main/java/org/apache/knox/gateway/services/knoxidf/federation/JdbcFederatedIdentityService.java b/gateway-server/src/main/java/org/apache/knox/gateway/services/knoxidf/federation/JdbcFederatedIdentityService.java index cf426b8369..8e26bdc1d5 100644 --- a/gateway-server/src/main/java/org/apache/knox/gateway/services/knoxidf/federation/JdbcFederatedIdentityService.java +++ b/gateway-server/src/main/java/org/apache/knox/gateway/services/knoxidf/federation/JdbcFederatedIdentityService.java @@ -18,59 +18,39 @@ import org.apache.knox.gateway.config.GatewayConfig; import org.apache.knox.gateway.database.DataSourceProvider; -import org.apache.knox.gateway.database.DatabaseType; -import org.apache.knox.gateway.database.JDBCUtils; +import org.apache.knox.gateway.i18n.messages.MessagesFactory; import org.apache.knox.gateway.services.ServiceLifecycleException; import org.apache.knox.gateway.services.security.AliasService; -import org.apache.knox.gateway.services.token.impl.TokenStateDatabase; -import javax.sql.DataSource; -import java.sql.Connection; -import java.sql.PreparedStatement; -import java.sql.ResultSet; import java.sql.SQLException; -import java.sql.Timestamp; -import java.util.HashMap; import java.util.Map; import java.util.Optional; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; -//TODO: split this class into service + FederatedIdentityDatabase classes public class JdbcFederatedIdentityService implements FederatedIdentityService { - private static final String FEDERATED_IDENTITY_TABLE_NAME = "federated_identity"; - private static final String FEDERATED_IDENTITY_ATTRIBUTES_TABLE_NAME = "federated_identity_attr"; + private static final FederatedIdentityServiceMessages LOG = MessagesFactory.get(FederatedIdentityServiceMessages.class); private final AtomicBoolean initialized = new AtomicBoolean(false); private final Lock initLock = new ReentrantLock(true); - private final DataSource dataSource; - - public JdbcFederatedIdentityService(GatewayConfig config, AliasService aliasService) throws Exception { - this.dataSource = DataSourceProvider.getDataSource(config, aliasService); - } - - private void ensureTablesExist(DatabaseType databaseType) { - try { - createTableIfNotExists(FEDERATED_IDENTITY_TABLE_NAME, databaseType.federatedIdentityTableSql()); - createTableIfNotExists(FEDERATED_IDENTITY_ATTRIBUTES_TABLE_NAME, databaseType.federatedIdentityAttrTableSql()); - } catch (Exception e) { - throw new RuntimeException(e); - } - } - - private void createTableIfNotExists(String tableName, String createSqlFileName) throws Exception { - if (!JDBCUtils.tableExists(tableName, dataSource)) { - JDBCUtils.createTableFromSQL(createSqlFileName, dataSource, TokenStateDatabase.class.getClassLoader()); - } - } + private AliasService aliasService; // connection username/pw are stored here + private FederatedIdentityDatabase federatedIdentityDatabase; @Override public void init(GatewayConfig config, Map options) throws ServiceLifecycleException { if (!initialized.get()) { initLock.lock(); try { - ensureTablesExist(DatabaseType.fromString(config.getDatabaseType())); + if (aliasService == null) { + throw new ServiceLifecycleException("The required AliasService reference has not been set."); + } + try { + this.federatedIdentityDatabase = new FederatedIdentityDatabase(DataSourceProvider.getDataSource(config, aliasService), config.getDatabaseType()); + initialized.set(true); + } catch (Exception e) { + throw new ServiceLifecycleException("Error while initiating JDBCTokenStateService: " + e, e); + } } finally { initLock.unlock(); } @@ -85,143 +65,44 @@ public void start() throws ServiceLifecycleException { public void stop() throws ServiceLifecycleException { } - @Override - public void addFederatedIdentity(FederatedIdentity identity) { - try (Connection c = dataSource.getConnection()) { - c.setAutoCommit(false); - - try (PreparedStatement ps = c.prepareStatement( - "INSERT INTO federated_identity " + - "(id, user_id, provider, external_subject, external_issuer, created_at) " + - "VALUES (?, ?, ?, ?, ?, ?)")) { - - ps.setString(1, identity.getId()); - ps.setString(2, identity.getUserId()); - ps.setString(3, identity.getProvider()); - ps.setString(4, identity.getExternalSubject()); - ps.setString(5, identity.getExternalIssuer()); - ps.setTimestamp(6, Timestamp.from(identity.getCreatedAt())); - ps.executeUpdate(); - } + public void setAliasService(AliasService aliasService) { + this.aliasService = aliasService; + } - try (PreparedStatement ps = c.prepareStatement( - "INSERT INTO federated_identity_attr " + - "(identity_id, attr_key, attr_value) VALUES (?, ?, ?)")) { + protected AliasService getAliasService() { + return aliasService; + } - for (var e : identity.getAttributes().entrySet()) { - ps.setString(1, identity.getId()); - ps.setString(2, e.getKey()); - ps.setString(3, e.getValue()); - ps.addBatch(); - } - ps.executeBatch(); + @Override + public void addFederatedIdentity(FederatedIdentity identity) { + try { + if (findByProviderAndSubject(identity.getProvider(), identity.getExternalIssuer(), identity.getExternalSubject()).isEmpty()) { + federatedIdentityDatabase.addFederatedIdentity(identity); } - - c.commit(); } catch (SQLException e) { - throw new RuntimeException(e); + LOG.errorSavingFederatedIdentityInDatabase(identity.getId(), e.getMessage(), e); + throw new FederatedIdentityServiceException("An error occurred while saving Federated Identity " + identity.getId() + " in the database", e); } } @Override - public Optional findByProviderAndSubject( - String provider, - String issuer, - String subject) { - - try (Connection c = dataSource.getConnection()) { - - FederatedIdentity federatedIdentity = null; - - try (PreparedStatement ps = c.prepareStatement("SELECT * FROM federated_identity " + - "WHERE provider = ? AND external_issuer = ? AND external_subject = ?")) { - ps.setString(1, provider); - ps.setString(2, issuer); - ps.setString(3, subject); - - try (ResultSet rs = ps.executeQuery()) { - if (!rs.next()) { - return Optional.empty(); - } - - federatedIdentity = new FederatedIdentity( - rs.getString("id"), - rs.getString("user_id"), - provider, - subject, - issuer, - rs.getTimestamp("created_at").toInstant(), new HashMap<>()); - } - } - - try (PreparedStatement ps = c.prepareStatement( - "SELECT attr_key, attr_value FROM federated_identity_attr WHERE identity_id = ?")) { - - ps.setString(1, federatedIdentity.getId()); - - try (ResultSet rs = ps.executeQuery()) { - while (rs.next()) { - federatedIdentity.getAttributes().put( - rs.getString(1), - rs.getString(2)); - } - } - } - - return Optional.of(federatedIdentity); + public Optional findByProviderAndSubject(String provider, String issuer, String subject) { + try { + return federatedIdentityDatabase.findByProviderAndSubject(provider, issuer, subject); } catch (SQLException e) { - throw new RuntimeException(e); + LOG.errorFetchingFederatedIdentityFromDatabase(provider, subject, issuer, e.getMessage(), e); } + return Optional.empty(); } @Override public Optional findById(String id) { - - try (Connection c = dataSource.getConnection()) { - - FederatedIdentity federatedIdentity; - - try (PreparedStatement ps = c.prepareStatement( - "SELECT id, user_id, provider, external_subject, external_issuer, created_at " + - "FROM federated_identity WHERE id = ?")) { - - ps.setString(1, id); - - try (ResultSet rs = ps.executeQuery()) { - if (!rs.next()) { - return Optional.empty(); - } - - federatedIdentity = new FederatedIdentity( - rs.getString("id"), - rs.getString("user_id"), - rs.getString("provider"), - rs.getString("external_subject"), - rs.getString("external_issuer"), - rs.getTimestamp("created_at").toInstant(), - new HashMap<>()); - } - } - - try (PreparedStatement ps = c.prepareStatement( - "SELECT attr_key, attr_value FROM federated_identity_attr WHERE identity_id = ?")) { - - ps.setString(1, federatedIdentity.getId()); - - try (ResultSet rs = ps.executeQuery()) { - while (rs.next()) { - federatedIdentity.getAttributes().put( - rs.getString("attr_key"), - rs.getString("attr_value")); - } - } - } - - return Optional.of(federatedIdentity); - + try { + return federatedIdentityDatabase.findById(id); } catch (SQLException e) { - throw new RuntimeException("Failed to load federated identity by id", e); + LOG.errorFetchingFederatedIdentityFromDatabase(id, e.getMessage(), e); } + return Optional.empty(); } } diff --git a/gateway-server/src/main/java/org/apache/knox/gateway/services/token/impl/TokenStateDatabase.java b/gateway-server/src/main/java/org/apache/knox/gateway/services/token/impl/TokenStateDatabase.java index dbc89d6950..c7579a4805 100644 --- a/gateway-server/src/main/java/org/apache/knox/gateway/services/token/impl/TokenStateDatabase.java +++ b/gateway-server/src/main/java/org/apache/knox/gateway/services/token/impl/TokenStateDatabase.java @@ -19,7 +19,7 @@ import org.apache.commons.codec.binary.Base64; import org.apache.knox.gateway.database.DatabaseType; -import org.apache.knox.gateway.database.JDBCUtils; +import org.apache.knox.gateway.database.KnoxDatabase; import org.apache.knox.gateway.services.security.token.KnoxToken; import org.apache.knox.gateway.services.security.token.TokenMetadata; @@ -37,7 +37,7 @@ import static java.nio.charset.StandardCharsets.UTF_8; -public class TokenStateDatabase { +public class TokenStateDatabase extends KnoxDatabase { static final String TOKENS_TABLE_NAME = "KNOX_TOKENS"; static final String TOKEN_METADATA_TABLE_NAME = "KNOX_TOKEN_METADATA"; private static final String ADD_TOKEN_SQL = "INSERT INTO " + TOKENS_TABLE_NAME + "(token_id, issue_time, expiration, max_lifetime) VALUES(?, ?, ?, ?)"; @@ -58,21 +58,13 @@ public class TokenStateDatabase { private static final String GET_TOKENS_CREATED_BY_USER_NAME_SQL = GET_ALL_TOKENS_SQL + " AND kt.token_id IN (SELECT token_id FROM " + TOKEN_METADATA_TABLE_NAME + " WHERE md_name = '" + TokenMetadata.CREATED_BY + "' AND md_value = ? )" + " ORDER BY kt.issue_time"; - private final DataSource dataSource; - TokenStateDatabase(DataSource dataSource, String dbType) throws Exception { - this.dataSource = dataSource; + super(dataSource); DatabaseType databaseType = DatabaseType.fromString(dbType); createTableIfNotExists(TOKENS_TABLE_NAME, databaseType.tokensTableSql()); createTableIfNotExists(TOKEN_METADATA_TABLE_NAME, databaseType.metadataTableSql()); } - private void createTableIfNotExists(String tableName, String createSqlFileName) throws Exception { - if (!JDBCUtils.tableExists(tableName, dataSource)) { - JDBCUtils.createTableFromSQL(createSqlFileName, dataSource, TokenStateDatabase.class.getClassLoader()); - } - } - boolean addToken(String tokenId, long issueTime, long expiration, long maxLifetimeDuration) throws SQLException { try (Connection connection = dataSource.getConnection(); PreparedStatement addTokenStatement = connection.prepareStatement(ADD_TOKEN_SQL)) { addTokenStatement.setString(1, tokenId); diff --git a/gateway-server/src/main/resources/createKnoxcloakFederatedIdentityAttributesTable.sql b/gateway-server/src/main/resources/createKnoxIDFFederatedIdentityAttributesTable.sql similarity index 100% rename from gateway-server/src/main/resources/createKnoxcloakFederatedIdentityAttributesTable.sql rename to gateway-server/src/main/resources/createKnoxIDFFederatedIdentityAttributesTable.sql diff --git a/gateway-server/src/main/resources/createKnoxcloakFederatedIdentityAttributesTableDerby.sql b/gateway-server/src/main/resources/createKnoxIDFFederatedIdentityAttributesTableDerby.sql similarity index 100% rename from gateway-server/src/main/resources/createKnoxcloakFederatedIdentityAttributesTableDerby.sql rename to gateway-server/src/main/resources/createKnoxIDFFederatedIdentityAttributesTableDerby.sql diff --git a/gateway-server/src/main/resources/createKnoxcloakFederatedIdentityAttributesTableOracle.sql b/gateway-server/src/main/resources/createKnoxIDFFederatedIdentityAttributesTableOracle.sql similarity index 100% rename from gateway-server/src/main/resources/createKnoxcloakFederatedIdentityAttributesTableOracle.sql rename to gateway-server/src/main/resources/createKnoxIDFFederatedIdentityAttributesTableOracle.sql diff --git a/gateway-server/src/main/resources/createKnoxcloakFederatedIdentityTable.sql b/gateway-server/src/main/resources/createKnoxIDFFederatedIdentityTable.sql similarity index 100% rename from gateway-server/src/main/resources/createKnoxcloakFederatedIdentityTable.sql rename to gateway-server/src/main/resources/createKnoxIDFFederatedIdentityTable.sql diff --git a/gateway-server/src/main/resources/createKnoxcloakFederatedIdentityTableDerby.sql b/gateway-server/src/main/resources/createKnoxIDFFederatedIdentityTableDerby.sql similarity index 100% rename from gateway-server/src/main/resources/createKnoxcloakFederatedIdentityTableDerby.sql rename to gateway-server/src/main/resources/createKnoxIDFFederatedIdentityTableDerby.sql diff --git a/gateway-server/src/main/resources/createKnoxcloakFederatedIdentityTableOracle.sql b/gateway-server/src/main/resources/createKnoxIDFFederatedIdentityTableOracle.sql similarity index 100% rename from gateway-server/src/main/resources/createKnoxcloakFederatedIdentityTableOracle.sql rename to gateway-server/src/main/resources/createKnoxIDFFederatedIdentityTableOracle.sql diff --git a/gateway-service-knoxidf/src/main/java/org/apache/knox/gateway/service/knoxidf/AuthorizeResource.java b/gateway-service-knoxidf/src/main/java/org/apache/knox/gateway/service/knoxidf/AuthorizeResource.java index cf98e938b0..abc351cea7 100644 --- a/gateway-service-knoxidf/src/main/java/org/apache/knox/gateway/service/knoxidf/AuthorizeResource.java +++ b/gateway-service-knoxidf/src/main/java/org/apache/knox/gateway/service/knoxidf/AuthorizeResource.java @@ -46,7 +46,7 @@ import org.apache.knox.gateway.util.knoxidf.AuthorizeRequestMetadata; import org.apache.knox.gateway.util.knoxidf.AuthorizeRequestMetadataStore; import org.apache.knox.gateway.util.knoxidf.FederatedOpConfiguration; -import org.apache.knox.gateway.util.knoxidf.FederatedOpConfigurationFactory; +import org.apache.knox.gateway.util.knoxidf.FederatedOpConfigurationStore; import org.apache.knox.gateway.util.knoxidf.KnoxIDFUtils; import javax.annotation.PostConstruct; @@ -89,8 +89,8 @@ public class AuthorizeResource extends PasscodeTokenResourceBase { "given_name", "family_name", "name", "locale"); private static final String UTF_8 = StandardCharsets.UTF_8.name(); - private Map federatedOpConfigurations; private AuthorizeRequestMetadataStore authorizeRequestMetadataStore; + private final FederatedOpConfigurationStore federatedOpConfigurationStore = FederatedOpConfigurationStore.getInstance(120000L); @Context private HttpServletRequest request; @@ -104,7 +104,6 @@ public class AuthorizeResource extends PasscodeTokenResourceBase { @Override public void init() throws ServletException, AliasServiceException, ServiceLifecycleException, KeyLengthException { super.init(); - this.federatedOpConfigurations = FederatedOpConfigurationFactory.createFederatedOpConfiguration(servletContext); this.authorizeRequestMetadataStore = AuthorizeRequestMetadataStore.getInstance(tokenTTL); final GatewayServices services = (GatewayServices) servletContext.getAttribute(GatewayServices.GATEWAY_SERVICES_ATTRIBUTE); federatedIdentityService = services.getService(ServiceType.KNOXIDF_FEDERATED_IDENTITY_SERVICE); @@ -130,7 +129,7 @@ public Response authorize(@QueryParam("response_type") String responseType, markConsentAccepted(authorizeRequestMetadata); } else { final String consentAuthState = UUID.randomUUID().toString(); - authorizeRequestMetadataStore.storeRequestMetadata(consentAuthState, authorizeRequestMetadata); + authorizeRequestMetadataStore.put(consentAuthState, authorizeRequestMetadata); final String baseUri = servletContext.getContextPath() + "/authConsent"; final String scopeParam = URLEncoder.encode(authorizeRequestMetadata.getJoinedRequestedScopes(), StandardCharsets.UTF_8); final String redirect = String.format(Locale.US, "%s?client_id=%s&state=%s&scope=%s", baseUri, clientId, consentAuthState, scopeParam); @@ -186,10 +185,11 @@ public Response authCallback() throws Exception { //This is the callback for the federated OP final String federatedAuthCode = request.getParameter("code"); final String state = request.getParameter("state"); - final AuthorizeRequestMetadata authorizeRequestMetadata = authorizeRequestMetadataStore.getRequestMetadata(state); - final String opName = authorizeRequestMetadata.getSelectedFederatedOpName(); - final Pair federatedTokens = exchangeFederatedAuthCodeToTokens(federatedAuthCode, federatedOpConfigurations.get(opName)); - final FederatedIdentity federatedIdentity = resolveFederatedIdentity(federatedTokens.getLeft(), opName); + final AuthorizeRequestMetadata authorizeRequestMetadata = authorizeRequestMetadataStore.get(state); + //at this point, there has to be exactly 1 federated OP config + final FederatedOpConfiguration federatedOpConfiguration = federatedOpConfigurationStore.get(state).stream().findFirst().get(); + final Pair federatedTokens = exchangeFederatedAuthCodeToTokens(federatedAuthCode, federatedOpConfiguration); + final FederatedIdentity federatedIdentity = resolveFederatedIdentity(federatedTokens.getLeft(), federatedOpConfiguration.getName()); return getAuthCodeFromKnox(authorizeRequestMetadata, Pair.of(federatedIdentity.getId(), federatedTokens.getRight())); } @@ -197,7 +197,7 @@ public Response authCallback() throws Exception { @Path("/consentAccepted") public Response consentAccepted() throws Exception { final String state = request.getParameter("state"); - final AuthorizeRequestMetadata authorizeRequestMetadata = authorizeRequestMetadataStore.getRequestMetadata(state); + final AuthorizeRequestMetadata authorizeRequestMetadata = authorizeRequestMetadataStore.get(state); if (authorizeRequestMetadata == null) { return error("Consent cannot be accepted", "Invalid state"); } @@ -287,7 +287,7 @@ private boolean matchesRedirectUri(String requestedUri, Set registeredUr private Pair exchangeFederatedAuthCodeToTokens(String federatedAuthCode, FederatedOpConfiguration opConfig) { String federatedIdToken = null; String federatedAccessToken = null; - final Response federatedTokenExchangeResponse = fetchFederatedTokens(federatedAuthCode, opConfig.getAuthorizeCallback(), opConfig); + final Response federatedTokenExchangeResponse = fetchFederatedTokens(federatedAuthCode, opConfig); if (federatedTokenExchangeResponse.getStatus() == Response.Status.OK.getStatusCode()) { final Map federatedTokenExchangeResponseBodyMap = JsonUtils.getMapFromJsonString((String) federatedTokenExchangeResponse.getEntity()); federatedIdToken = federatedTokenExchangeResponseBodyMap.get("id_token"); @@ -298,10 +298,10 @@ private Pair exchangeFederatedAuthCodeToTokens(String federatedA } } - private Response fetchFederatedTokens(final String code, final String redirectUri, FederatedOpConfiguration opConfig) { + private Response fetchFederatedTokens(final String code, FederatedOpConfiguration opConfig) { final List params = new ArrayList<>(); params.add(new BasicNameValuePair("code", code)); - params.add(new BasicNameValuePair("redirect_uri", redirectUri)); + params.add(new BasicNameValuePair("redirect_uri", opConfig.getAuthorizeCallback())); params.add(new BasicNameValuePair("grant_type", "authorization_code")); params.add(new BasicNameValuePair("client_id", opConfig.getClientId())); params.add(new BasicNameValuePair("client_secret", opConfig.getClientSecret())); diff --git a/gateway-service-knoxsso/src/main/java/org/apache/knox/gateway/service/knoxsso/WebSSOResource.java b/gateway-service-knoxsso/src/main/java/org/apache/knox/gateway/service/knoxsso/WebSSOResource.java index b1e30b554f..5da9f343eb 100644 --- a/gateway-service-knoxsso/src/main/java/org/apache/knox/gateway/service/knoxsso/WebSSOResource.java +++ b/gateway-service-knoxsso/src/main/java/org/apache/knox/gateway/service/knoxsso/WebSSOResource.java @@ -41,10 +41,8 @@ import org.apache.knox.gateway.util.Tokens; import org.apache.knox.gateway.util.Urls; import org.apache.knox.gateway.util.WhitelistUtils; -import org.apache.knox.gateway.util.knoxidf.AuthorizeRequestMetadata; -import org.apache.knox.gateway.util.knoxidf.AuthorizeRequestMetadataStore; import org.apache.knox.gateway.util.knoxidf.FederatedOpConfiguration; -import org.apache.knox.gateway.util.knoxidf.FederatedOpConfigurationFactory; +import org.apache.knox.gateway.util.knoxidf.FederatedOpConfigurationStore; import org.apache.knox.gateway.util.knoxidf.KnoxIDFUtils; import javax.annotation.PostConstruct; @@ -67,10 +65,11 @@ import java.security.Principal; import java.util.ArrayList; import java.util.Arrays; -import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; +import java.util.Optional; +import java.util.Set; import static javax.ws.rs.core.MediaType.APPLICATION_JSON; import static javax.ws.rs.core.MediaType.APPLICATION_XML; @@ -120,8 +119,7 @@ public class WebSSOResource { private String clusterName; private String tokenIssuer; private TokenStateService tokenStateService; - private final Map federatedOpConfigurations = null; - private final AuthorizeRequestMetadataStore authorizeRequestMetadataStore = AuthorizeRequestMetadataStore.getInstance(120000L); + private final FederatedOpConfigurationStore federatedOpConfigurationStore = FederatedOpConfigurationStore.getInstance(120000L); private String sameSiteValue; @@ -238,11 +236,18 @@ private void handleCookieSetup() { public Response federatedOpLogin() { final String loginSessionId = request.getParameter("fedOpSid"); final String opName = request.getParameter("fedOpName"); - final AuthorizeRequestMetadata metadata = authorizeRequestMetadataStore.getRequestMetadata(loginSessionId); - final FederatedOpConfiguration federatedOpConfiguration = FederatedOpConfigurationFactory.createFederatedOpConfiguration(context).get(opName); - metadata.setSelectedFederatedOpName(opName); - final String federatedOpAuthRedirect = KnoxIDFUtils.buildFederatedOpAuthRedirect(federatedOpConfiguration, loginSessionId); - return Response.seeOther(java.net.URI.create(federatedOpAuthRedirect)).build(); + final Optional federatedOpConfig = federatedOpConfigurationStore.get(loginSessionId).stream() + .filter(federatedOpConfiguration -> federatedOpConfiguration.getName().equals(opName)) + .findFirst(); + if (federatedOpConfig.isPresent()) { + final FederatedOpConfiguration federatedOpConfiguration = federatedOpConfig.get(); + //keep only the selected federated OP in the cache -> we can easily get it in the AuthorizeResource.authCallback endpoint + federatedOpConfigurationStore.put(loginSessionId, Set.of(federatedOpConfiguration)); + final String federatedOpAuthRedirect = KnoxIDFUtils.buildFederatedOpAuthRedirect(federatedOpConfiguration, loginSessionId); + return Response.seeOther(java.net.URI.create(federatedOpAuthRedirect)).build(); + } else { + return KnoxIDFUtils.error("invalid_request", "Cannot load federated op config associated with login session"); + } } @GET diff --git a/gateway-spi/src/main/java/org/apache/knox/gateway/services/knoxidf/federation/FederatedIdentityServiceException.java b/gateway-spi/src/main/java/org/apache/knox/gateway/services/knoxidf/federation/FederatedIdentityServiceException.java new file mode 100644 index 0000000000..30cbce7b24 --- /dev/null +++ b/gateway-spi/src/main/java/org/apache/knox/gateway/services/knoxidf/federation/FederatedIdentityServiceException.java @@ -0,0 +1,28 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package org.apache.knox.gateway.services.knoxidf.federation; + +public class FederatedIdentityServiceException extends RuntimeException { + + public FederatedIdentityServiceException(String message) { + super(message); + } + + public FederatedIdentityServiceException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/gateway-util-common/src/main/java/org/apache/knox/gateway/util/knoxidf/AuthorizeRequestMetadata.java b/gateway-util-common/src/main/java/org/apache/knox/gateway/util/knoxidf/AuthorizeRequestMetadata.java index a3dca7e41f..9042b2ea97 100644 --- a/gateway-util-common/src/main/java/org/apache/knox/gateway/util/knoxidf/AuthorizeRequestMetadata.java +++ b/gateway-util-common/src/main/java/org/apache/knox/gateway/util/knoxidf/AuthorizeRequestMetadata.java @@ -30,8 +30,6 @@ public final class AuthorizeRequestMetadata { private final String state; private final String nonce; - private String selectedFederatedOpName; - public AuthorizeRequestMetadata(String clientId, String subject, String responseType, String redirectUri, Set requestedScopes, String state, String nonce) { this.clientId = clientId; this.subject = subject; @@ -107,15 +105,4 @@ public String getJoinedRequestedScopes() { return String.join(" ", requestedScopes); } - public String getSelectedFederatedOpName() { - return selectedFederatedOpName; - } - - public void setSelectedFederatedOpName(String selectedFederatedOpName) { - this.selectedFederatedOpName = selectedFederatedOpName; - } - - public boolean hasSelectedFederatedOpName() { - return selectedFederatedOpName != null && !selectedFederatedOpName.isEmpty(); - } } diff --git a/gateway-util-common/src/main/java/org/apache/knox/gateway/util/knoxidf/AuthorizeRequestMetadataStore.java b/gateway-util-common/src/main/java/org/apache/knox/gateway/util/knoxidf/AuthorizeRequestMetadataStore.java index 96311a6b48..80f3d7110f 100644 --- a/gateway-util-common/src/main/java/org/apache/knox/gateway/util/knoxidf/AuthorizeRequestMetadataStore.java +++ b/gateway-util-common/src/main/java/org/apache/knox/gateway/util/knoxidf/AuthorizeRequestMetadataStore.java @@ -16,32 +16,18 @@ */ package org.apache.knox.gateway.util.knoxidf; -import com.github.benmanes.caffeine.cache.Cache; -import com.github.benmanes.caffeine.cache.Caffeine; - -import java.util.concurrent.TimeUnit; - -public class AuthorizeRequestMetadataStore { +public class AuthorizeRequestMetadataStore extends KnoxIDFArtifactStore{ private static AuthorizeRequestMetadataStore instance; - private final Cache authorizeRequestMetadataCache; - private AuthorizeRequestMetadataStore(final long ttl) { - this.authorizeRequestMetadataCache = Caffeine.newBuilder().expireAfterWrite(ttl * 2, TimeUnit.MILLISECONDS).build(); + private AuthorizeRequestMetadataStore(long ttl) { + super(ttl); } - public static synchronized AuthorizeRequestMetadataStore getInstance(final long ttl) { + public static synchronized AuthorizeRequestMetadataStore getInstance(long ttl) { if (instance == null) { instance = new AuthorizeRequestMetadataStore(ttl); } return instance; } - - public void storeRequestMetadata(String state, AuthorizeRequestMetadata requestMetadata) { - authorizeRequestMetadataCache.put(state, requestMetadata); - } - - public AuthorizeRequestMetadata getRequestMetadata(String state) { - return authorizeRequestMetadataCache.getIfPresent(state); - } } diff --git a/gateway-util-common/src/main/java/org/apache/knox/gateway/util/knoxidf/FederatedOpConfigurationStore.java b/gateway-util-common/src/main/java/org/apache/knox/gateway/util/knoxidf/FederatedOpConfigurationStore.java new file mode 100644 index 0000000000..80bbb1a04f --- /dev/null +++ b/gateway-util-common/src/main/java/org/apache/knox/gateway/util/knoxidf/FederatedOpConfigurationStore.java @@ -0,0 +1,35 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package org.apache.knox.gateway.util.knoxidf; + +import java.util.Set; + +public class FederatedOpConfigurationStore extends KnoxIDFArtifactStore> { + + private static FederatedOpConfigurationStore instance; + + private FederatedOpConfigurationStore(long ttl) { + super(ttl); + } + + public static synchronized FederatedOpConfigurationStore getInstance(long ttl) { + if (instance == null) { + instance = new FederatedOpConfigurationStore(ttl); + } + return instance; + } +} diff --git a/gateway-util-common/src/main/java/org/apache/knox/gateway/util/knoxidf/KnoxIDFArtifactStore.java b/gateway-util-common/src/main/java/org/apache/knox/gateway/util/knoxidf/KnoxIDFArtifactStore.java new file mode 100644 index 0000000000..dbeba8141d --- /dev/null +++ b/gateway-util-common/src/main/java/org/apache/knox/gateway/util/knoxidf/KnoxIDFArtifactStore.java @@ -0,0 +1,39 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package org.apache.knox.gateway.util.knoxidf; + +import com.github.benmanes.caffeine.cache.Cache; +import com.github.benmanes.caffeine.cache.Caffeine; + +import java.util.concurrent.TimeUnit; + +public abstract class KnoxIDFArtifactStore { + + private final Cache cache; + + protected KnoxIDFArtifactStore(long ttl) { + this.cache = Caffeine.newBuilder().expireAfterWrite(ttl * 2, TimeUnit.MILLISECONDS).build(); + } + + public void put(String key, T value) { + cache.put(key, value); + } + + public T get(String key) { + return cache.getIfPresent(key); + } +} diff --git a/gateway-util-common/src/main/java/org/apache/knox/gateway/util/knoxidf/KnoxIDFConstants.java b/gateway-util-common/src/main/java/org/apache/knox/gateway/util/knoxidf/KnoxIDFConstants.java index bc8db9e48a..223241fd77 100644 --- a/gateway-util-common/src/main/java/org/apache/knox/gateway/util/knoxidf/KnoxIDFConstants.java +++ b/gateway-util-common/src/main/java/org/apache/knox/gateway/util/knoxidf/KnoxIDFConstants.java @@ -40,5 +40,5 @@ public interface KnoxIDFConstants { String FEDERATED_ACCESS_TOKEN_PREFIX = "fed_access_"; String FEDERATED_OP_CONFIG_PREFIX = "federated.op."; String FEDERATED_OP_CONFIG_ENABLED = FEDERATED_OP_CONFIG_PREFIX + "enabled"; - String FEDERATED_OP_CONFIG_NAME = FEDERATED_OP_CONFIG_PREFIX + "name"; + String FEDERATED_OP_CONFIG_NAME = FEDERATED_OP_CONFIG_PREFIX + "names"; } From 2854240dda0be15dafb8c1506f9c1ca54642b118 Mon Sep 17 00:00:00 2001 From: Sandor Molnar Date: Mon, 27 Apr 2026 11:19:31 +0200 Subject: [PATCH 07/14] KnoxIDF - Multi OP enablement improvements and code adoption to Larry's recent changes --- .../jwt/filter/SSOCookieFederationFilter.java | 2 +- .../service/knoxidf/TokenResource.java | 21 +++++++++++++------ .../knoxidf/FederatedOpConfiguration.java | 15 +++++++------ .../FederatedOpConfigurationFactory.java | 14 ++++++------- .../util/knoxidf/KnoxIDFConstants.java | 2 +- 5 files changed, 30 insertions(+), 24 deletions(-) diff --git a/gateway-provider-security-jwt/src/main/java/org/apache/knox/gateway/provider/federation/jwt/filter/SSOCookieFederationFilter.java b/gateway-provider-security-jwt/src/main/java/org/apache/knox/gateway/provider/federation/jwt/filter/SSOCookieFederationFilter.java index 8609f76b81..83b080dafa 100644 --- a/gateway-provider-security-jwt/src/main/java/org/apache/knox/gateway/provider/federation/jwt/filter/SSOCookieFederationFilter.java +++ b/gateway-provider-security-jwt/src/main/java/org/apache/knox/gateway/provider/federation/jwt/filter/SSOCookieFederationFilter.java @@ -351,7 +351,7 @@ protected String constructLoginURL(HttpServletRequest request) throws Unsupporte final String loginSessionId = request.getSession().getId(); authorizeRequestMetadataStore.put(loginSessionId, KnoxIDFUtils.buildAuthRequestMetadata(request)); federatedOpConfigurationStore.put(loginSessionId, enabledFederatedOpConfigs); - final List opNames = enabledFederatedOpConfigs.stream().map(FederatedOpConfiguration::getName).collect(Collectors.toList()); + final List opNames = enabledFederatedOpConfigs.stream().map(FederatedOpConfiguration::getName).sorted().collect(Collectors.toList()); providerURL += delimiter + "federatedOpLoginSession=" + URLEncoder.encode(loginSessionId, StandardCharsets.UTF_8) + "&federatedOpNames=" + URLEncoder.encode(String.join(",", opNames), StandardCharsets.UTF_8); diff --git a/gateway-service-knoxidf/src/main/java/org/apache/knox/gateway/service/knoxidf/TokenResource.java b/gateway-service-knoxidf/src/main/java/org/apache/knox/gateway/service/knoxidf/TokenResource.java index fbbffd2ff0..d0c44fdddf 100644 --- a/gateway-service-knoxidf/src/main/java/org/apache/knox/gateway/service/knoxidf/TokenResource.java +++ b/gateway-service-knoxidf/src/main/java/org/apache/knox/gateway/service/knoxidf/TokenResource.java @@ -31,6 +31,7 @@ import org.apache.knox.gateway.services.security.token.TokenServiceException; import org.apache.knox.gateway.services.security.token.UnknownTokenException; import org.apache.knox.gateway.services.security.token.impl.JWT; +import org.apache.knox.gateway.util.ServletRequestUtils; import javax.annotation.PostConstruct; import javax.servlet.ServletContext; @@ -80,8 +81,8 @@ public void init() throws ServletException, AliasServiceException, ServiceLifecy @Override @POST public Response doPost() { - final String code = request.getParameter("code"); - final String redirectUri = request.getParameter("redirect_uri"); + final String code = getRequestParam("code"); + final String redirectUri = getRequestParam("redirect_uri"); final Response paramVerificationErrorResponse = verifyParams(code, redirectUri); if (paramVerificationErrorResponse == null) { @@ -104,7 +105,7 @@ public Response doPost() { @Override protected UserContext buildUserContext(HttpServletRequest request) { try { - final String code = request.getParameter("code"); + final String code = getRequestParam("code"); final TokenMetadata tokenMetadata = tokenStateService.getTokenMetadata(code); final String scope = tokenMetadata.getMetadata("scope"); final Map userParams = userParamsProvider.getParamsFor(tokenMetadata.getUserName(), scope); @@ -120,7 +121,7 @@ protected UserContext buildUserContext(HttpServletRequest request) { protected void addArbitraryTokenMetadata(TokenMetadata tokenMetadata) { try { super.addArbitraryTokenMetadata(tokenMetadata); - final String code = request.getParameter("code"); + final String code = getRequestParam("code"); final TokenMetadata authCodeTokenMetadata = tokenStateService.getTokenMetadata(code); //if the auth code token was a result of a federated OIDC call, we need to save the associated @@ -144,7 +145,7 @@ protected ResponseMap buildResponseMap(JWT token, long expires) throws TokenServ private String generateIdToken(JWT accessToken) throws TokenServiceException { try { - final String code = request.getParameter("code"); + final String code = getRequestParam("code"); final TokenMetadata tokenMetadata = tokenStateService.getTokenMetadata(code); final boolean hasFederatedIdToken = StringUtils.isNotBlank(tokenMetadata.getMetadata("federated_identity_id")); @@ -234,7 +235,7 @@ private void validateAuthCode(String code, String redirectUri) throws AuthTokenV } else if (!associateRedirectUri.equals(redirectUri)) { throw new AuthTokenValidationError("Invalid redirect_uri: " + redirectUri); } else { - final String clientId = request.getParameter("client_id"); + final String clientId = getRequestParam("client_id"); if (!associatedClientId.equals(clientId)) { throw new AuthTokenValidationError("Invalid client_id: " + clientId); } @@ -244,6 +245,14 @@ private void validateAuthCode(String code, String redirectUri) throws AuthTokenV } } + private String getRequestParam(String paramName) { + String requestParamValue = request.getParameter(paramName); + if (requestParamValue == null) { + requestParamValue = ServletRequestUtils.unwrapHttpServletRequest(request).getParameter(paramName); + } + return requestParamValue; + } + private static class AuthTokenValidationError extends Exception { AuthTokenValidationError(String message) { super(message); diff --git a/gateway-util-common/src/main/java/org/apache/knox/gateway/util/knoxidf/FederatedOpConfiguration.java b/gateway-util-common/src/main/java/org/apache/knox/gateway/util/knoxidf/FederatedOpConfiguration.java index c385ad5ed7..1ba8498a96 100644 --- a/gateway-util-common/src/main/java/org/apache/knox/gateway/util/knoxidf/FederatedOpConfiguration.java +++ b/gateway-util-common/src/main/java/org/apache/knox/gateway/util/knoxidf/FederatedOpConfiguration.java @@ -18,7 +18,7 @@ import javax.servlet.ServletContext; -public class FederatedOpConfiguration { +public class FederatedOpConfiguration implements Comparable { private final boolean enabled; private final String name; private final String clientId; @@ -29,14 +29,10 @@ public class FederatedOpConfiguration { private final String discoveryEndpoint; private final String authorizeCallback; - public FederatedOpConfiguration(final ServletContext servletContext) { - this(servletContext, servletContext.getInitParameter(KnoxIDFConstants.FEDERATED_OP_CONFIG_NAME)); - } - public FederatedOpConfiguration(final ServletContext servletContext, final String opName) { - this.enabled = Boolean.parseBoolean(servletContext.getInitParameter(KnoxIDFConstants.FEDERATED_OP_CONFIG_ENABLED)); this.name = opName; final String prefix = KnoxIDFConstants.FEDERATED_OP_CONFIG_PREFIX + (opName != null ? opName + "." : ""); + this.enabled = Boolean.parseBoolean(servletContext.getInitParameter(prefix + "enabled")); this.clientId = servletContext.getInitParameter(prefix + "clientId"); this.clientSecret = servletContext.getInitParameter(prefix + "clientSecret"); this.tokenEndpoint = servletContext.getInitParameter(prefix + "token.endpoint"); @@ -50,11 +46,10 @@ public String getName() { return name; } - public boolean isFederatedOpRedirectEnabled() { + public boolean isEnabled() { return enabled; } - public String getClientId() { return clientId; } @@ -83,4 +78,8 @@ public String getDiscoveryEndpoint() { return discoveryEndpoint; } + @Override + public int compareTo(FederatedOpConfiguration other) { + return this.name.compareTo(other.name); + } } diff --git a/gateway-util-common/src/main/java/org/apache/knox/gateway/util/knoxidf/FederatedOpConfigurationFactory.java b/gateway-util-common/src/main/java/org/apache/knox/gateway/util/knoxidf/FederatedOpConfigurationFactory.java index b4fc5d4e5d..f1efc3d75b 100644 --- a/gateway-util-common/src/main/java/org/apache/knox/gateway/util/knoxidf/FederatedOpConfigurationFactory.java +++ b/gateway-util-common/src/main/java/org/apache/knox/gateway/util/knoxidf/FederatedOpConfigurationFactory.java @@ -24,20 +24,18 @@ public class FederatedOpConfigurationFactory { public static Map createFederatedOpConfiguration(final ServletContext servletContext) { - final String enabled = servletContext.getInitParameter(KnoxIDFConstants.FEDERATED_OP_CONFIG_ENABLED); - if (!Boolean.parseBoolean(enabled)) { - return Collections.emptyMap(); - } - - final String names = servletContext.getInitParameter(KnoxIDFConstants.FEDERATED_OP_CONFIG_NAME); + final String names = servletContext.getInitParameter(KnoxIDFConstants.FEDERATED_OP_CONFIG_NAMES); if (names == null || names.isEmpty()) { return Collections.emptyMap(); } final Map configs = new HashMap<>(); for (String name : names.split(",")) { - String trimmedName = name.trim(); - configs.put(trimmedName, new FederatedOpConfiguration(servletContext, trimmedName)); + final String trimmedName = name.trim(); + final FederatedOpConfiguration federatedOpConfiguration = new FederatedOpConfiguration(servletContext, trimmedName); + if (federatedOpConfiguration.isEnabled()) { + configs.put(trimmedName, federatedOpConfiguration); + } } return configs; } diff --git a/gateway-util-common/src/main/java/org/apache/knox/gateway/util/knoxidf/KnoxIDFConstants.java b/gateway-util-common/src/main/java/org/apache/knox/gateway/util/knoxidf/KnoxIDFConstants.java index 223241fd77..b19c98f130 100644 --- a/gateway-util-common/src/main/java/org/apache/knox/gateway/util/knoxidf/KnoxIDFConstants.java +++ b/gateway-util-common/src/main/java/org/apache/knox/gateway/util/knoxidf/KnoxIDFConstants.java @@ -40,5 +40,5 @@ public interface KnoxIDFConstants { String FEDERATED_ACCESS_TOKEN_PREFIX = "fed_access_"; String FEDERATED_OP_CONFIG_PREFIX = "federated.op."; String FEDERATED_OP_CONFIG_ENABLED = FEDERATED_OP_CONFIG_PREFIX + "enabled"; - String FEDERATED_OP_CONFIG_NAME = FEDERATED_OP_CONFIG_PREFIX + "names"; + String FEDERATED_OP_CONFIG_NAMES = FEDERATED_OP_CONFIG_PREFIX + "names"; } From 9e37b73e0620ebba38cc30740324f26289583850 Mon Sep 17 00:00:00 2001 From: Sandor Molnar Date: Mon, 27 Apr 2026 12:42:30 +0200 Subject: [PATCH 08/14] KnoxIDF - code cleanup, round 2 --- .../jwt/filter/SSOCookieFederationFilter.java | 3 +-- .../src/main/resources/docker/Dockerfile | 2 -- .../resources/docker/gateway-entrypoint.sh | 7 ------- .../gateway/filter/AnonymousAuthFilter.java | 1 - .../jwt/filter/JWTFederationFilter.java | 20 ++++++++++++------- .../jwt/filter/SSOCookieFederationFilter.java | 9 ++++++--- .../federation/SSOCookieProviderTest.java | 2 +- ...pache.knox.gateway.services.ServiceFactory | 2 ++ .../service/knoxidf/UserInfoResource.java | 6 ++++-- .../knoxidf/FederatedOpConfiguration.java | 6 +----- .../util/knoxidf/KnoxIDFConstants.java | 5 +++-- 11 files changed, 31 insertions(+), 32 deletions(-) diff --git a/gateway-adapter/src/main/java/org/apache/hadoop/gateway/provider/federation/jwt/filter/SSOCookieFederationFilter.java b/gateway-adapter/src/main/java/org/apache/hadoop/gateway/provider/federation/jwt/filter/SSOCookieFederationFilter.java index b1f6696d6a..d4e4340721 100644 --- a/gateway-adapter/src/main/java/org/apache/hadoop/gateway/provider/federation/jwt/filter/SSOCookieFederationFilter.java +++ b/gateway-adapter/src/main/java/org/apache/hadoop/gateway/provider/federation/jwt/filter/SSOCookieFederationFilter.java @@ -19,7 +19,6 @@ import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; -import java.io.UnsupportedEncodingException; /** * An adapter class that delegate calls to @@ -47,7 +46,7 @@ protected void handleValidationError(HttpServletRequest request, * @return url to use as login url for redirect */ @Override - protected String constructLoginURL(HttpServletRequest request) throws UnsupportedEncodingException { + protected String constructLoginURL(HttpServletRequest request) { return super.constructLoginURL(request); } } diff --git a/gateway-docker/src/main/resources/docker/Dockerfile b/gateway-docker/src/main/resources/docker/Dockerfile index 91b5d82017..7121de75d5 100644 --- a/gateway-docker/src/main/resources/docker/Dockerfile +++ b/gateway-docker/src/main/resources/docker/Dockerfile @@ -12,8 +12,6 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -FROM docker-private.infra.cloudera.com/cloudera_base/hardened/cloudera-openjdk:jdk-17-runtime-nofips - FROM dhi.io/eclipse-temurin:17-jdk-debian13-dev AS build LABEL maintainer="Apache Knox " diff --git a/gateway-docker/src/main/resources/docker/gateway-entrypoint.sh b/gateway-docker/src/main/resources/docker/gateway-entrypoint.sh index c0fb8ac99a..2cb0277a3d 100755 --- a/gateway-docker/src/main/resources/docker/gateway-entrypoint.sh +++ b/gateway-docker/src/main/resources/docker/gateway-entrypoint.sh @@ -249,12 +249,5 @@ done export KNOX_GATEWAY_DBG_OPTS="${KNOX_GATEWAY_DBG_OPTS} -Djavax.net.ssl.trustStore=${KEYSTORE_DIR}/truststore.jks -Djavax.net.ssl.trustStorePassword=${ALIAS_PASSPHRASE}" -# Setup DB for knoxidf demo -/home/knox/knox/bin/knoxcli.sh create-alias gateway_database_user --value postgres -/home/knox/knox/bin/knoxcli.sh create-alias gateway_database_password --value myPassword - -# Setup Knox token hash key for the demo (so that I can re-use the same clientId everywhere) -/home/knox/knox/bin/knoxcli.sh create-alias knox.token.hash.key --value B5oxlx9M4h4MhaGakj8k7Q2fbmPzo6h9te8dWTHs5Mg - echo "Starting Knox gateway ..." /home/knox/knox/bin/gateway.sh start diff --git a/gateway-provider-security-authc-anon/src/main/java/org/apache/knox/gateway/filter/AnonymousAuthFilter.java b/gateway-provider-security-authc-anon/src/main/java/org/apache/knox/gateway/filter/AnonymousAuthFilter.java index 19fd64cc1e..b0b2a85d8a 100755 --- a/gateway-provider-security-authc-anon/src/main/java/org/apache/knox/gateway/filter/AnonymousAuthFilter.java +++ b/gateway-provider-security-authc-anon/src/main/java/org/apache/knox/gateway/filter/AnonymousAuthFilter.java @@ -76,7 +76,6 @@ private void continueWithEstablishedSecurityContext(Subject subject, final HttpS new PrivilegedExceptionAction() { @Override public Object run() throws Exception { - request.getParameter("code"); chain.doFilter(request, response); return null; } diff --git a/gateway-provider-security-jwt/src/main/java/org/apache/knox/gateway/provider/federation/jwt/filter/JWTFederationFilter.java b/gateway-provider-security-jwt/src/main/java/org/apache/knox/gateway/provider/federation/jwt/filter/JWTFederationFilter.java index 098f28247a..ffa5d0868e 100644 --- a/gateway-provider-security-jwt/src/main/java/org/apache/knox/gateway/provider/federation/jwt/filter/JWTFederationFilter.java +++ b/gateway-provider-security-jwt/src/main/java/org/apache/knox/gateway/provider/federation/jwt/filter/JWTFederationFilter.java @@ -30,6 +30,7 @@ import org.apache.knox.gateway.util.CertificateUtils; import org.apache.knox.gateway.util.CookieUtils; import org.apache.knox.gateway.util.ServletRequestUtils; +import org.apache.knox.gateway.util.knoxidf.KnoxIDFConstants; import javax.security.auth.Subject; import javax.servlet.FilterChain; @@ -190,11 +191,8 @@ public void doFilter(ServletRequest request, ServletResponse response, FilterCha try { JWT token = new JWTToken(tokenValue); if (validateToken((HttpServletRequest) request, (HttpServletResponse) response, chain, token)) { - Subject subject = createSubjectFromToken(token); - request.setAttribute("X-Token-Id", TokenUtils.getTokenId(token)); - if (token.getClaim("scope") != null) { - request.setAttribute("X-Token-Scope", token.getClaim("scope")); - } + final Subject subject = createSubjectFromToken(token); + addKnoxIDFAttributes(request, token); continueWithEstablishedSecurityContext(subject, (HttpServletRequest) request, (HttpServletResponse) response, chain); } } catch (ParseException | UnknownTokenException ex) { @@ -216,8 +214,8 @@ public void doFilter(ServletRequest request, ServletResponse response, FilterCha } if (validateToken((HttpServletRequest) request, (HttpServletResponse) response, chain, tokenId, passcode)) { try { - Subject subject = createSubjectFromTokenIdentifier(tokenId); - request.setAttribute("X-Token-Id", tokenId); + final Subject subject = createSubjectFromTokenIdentifier(tokenId); + request.setAttribute(KnoxIDFConstants.TOKEN_ID_ATTRIBUTE, tokenId); continueWithEstablishedSecurityContext(subject, (HttpServletRequest) request, (HttpServletResponse) response, chain); } catch (UnknownTokenException e) { ((HttpServletResponse) response).sendError(HttpServletResponse.SC_UNAUTHORIZED); @@ -231,6 +229,14 @@ public void doFilter(ServletRequest request, ServletResponse response, FilterCha } } + private static void addKnoxIDFAttributes(ServletRequest request, JWT token) { + request.setAttribute(KnoxIDFConstants.TOKEN_ID_ATTRIBUTE, TokenUtils.getTokenId(token)); + final String scope = token.getClaim(KnoxIDFConstants.SCOPE); + if (scope != null) { + request.setAttribute(KnoxIDFConstants.SCOPE_ATTRIBUTE, token.getClaim(scope)); + } + } + private void validateClientID(HttpServletRequest request, String tokenValue) { final String clientID = request.getParameter(CLIENT_ID); validateClientID(clientID, tokenValue); diff --git a/gateway-provider-security-jwt/src/main/java/org/apache/knox/gateway/provider/federation/jwt/filter/SSOCookieFederationFilter.java b/gateway-provider-security-jwt/src/main/java/org/apache/knox/gateway/provider/federation/jwt/filter/SSOCookieFederationFilter.java index 83b080dafa..7558f5a2ed 100644 --- a/gateway-provider-security-jwt/src/main/java/org/apache/knox/gateway/provider/federation/jwt/filter/SSOCookieFederationFilter.java +++ b/gateway-provider-security-jwt/src/main/java/org/apache/knox/gateway/provider/federation/jwt/filter/SSOCookieFederationFilter.java @@ -47,13 +47,13 @@ import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; -import java.io.UnsupportedEncodingException; import java.net.MalformedURLException; import java.net.URL; import java.net.URLEncoder; import java.nio.charset.StandardCharsets; import java.text.ParseException; import java.util.ArrayList; +import java.util.Comparator; import java.util.HashSet; import java.util.List; import java.util.Set; @@ -333,7 +333,7 @@ private String constructGlobalLogoutUrl(HttpServletRequest request) { * @param request for getting the original request URL * @return url to use as login url for redirect */ - protected String constructLoginURL(HttpServletRequest request) throws UnsupportedEncodingException { + protected String constructLoginURL(HttpServletRequest request) { String providerURL = null; String delimiter = "?"; if (authenticationProviderUrl == null) { @@ -351,7 +351,10 @@ protected String constructLoginURL(HttpServletRequest request) throws Unsupporte final String loginSessionId = request.getSession().getId(); authorizeRequestMetadataStore.put(loginSessionId, KnoxIDFUtils.buildAuthRequestMetadata(request)); federatedOpConfigurationStore.put(loginSessionId, enabledFederatedOpConfigs); - final List opNames = enabledFederatedOpConfigs.stream().map(FederatedOpConfiguration::getName).sorted().collect(Collectors.toList()); + final List opNames = enabledFederatedOpConfigs.stream() + .sorted(Comparator.comparing(FederatedOpConfiguration::getName)) + .map(FederatedOpConfiguration::getName) + .collect(Collectors.toList()); providerURL += delimiter + "federatedOpLoginSession=" + URLEncoder.encode(loginSessionId, StandardCharsets.UTF_8) + "&federatedOpNames=" + URLEncoder.encode(String.join(",", opNames), StandardCharsets.UTF_8); diff --git a/gateway-provider-security-jwt/src/test/java/org/apache/knox/gateway/provider/federation/SSOCookieProviderTest.java b/gateway-provider-security-jwt/src/test/java/org/apache/knox/gateway/provider/federation/SSOCookieProviderTest.java index f3457fb621..69df31383b 100644 --- a/gateway-provider-security-jwt/src/test/java/org/apache/knox/gateway/provider/federation/SSOCookieProviderTest.java +++ b/gateway-provider-security-jwt/src/test/java/org/apache/knox/gateway/provider/federation/SSOCookieProviderTest.java @@ -739,7 +739,7 @@ private static class TestSSOCookieFederationProvider extends SSOCookieFederation private int verificationCount; @Override - public String constructLoginURL(HttpServletRequest req) throws UnsupportedEncodingException { + public String constructLoginURL(HttpServletRequest req) { return super.constructLoginURL(req); } diff --git a/gateway-server/src/main/resources/META-INF/services/org.apache.knox.gateway.services.ServiceFactory b/gateway-server/src/main/resources/META-INF/services/org.apache.knox.gateway.services.ServiceFactory index 83f6b04ee3..c48af4a587 100644 --- a/gateway-server/src/main/resources/META-INF/services/org.apache.knox.gateway.services.ServiceFactory +++ b/gateway-server/src/main/resources/META-INF/services/org.apache.knox.gateway.services.ServiceFactory @@ -16,6 +16,8 @@ # limitations under the License. ########################################################################## +# Please keep the alphabetical order of service factories! + org.apache.knox.gateway.services.factory.AliasServiceFactory org.apache.knox.gateway.services.factory.ConcurrentSessionVerifierFactory org.apache.knox.gateway.services.factory.ClusterConfigurationMonitorServiceFactory diff --git a/gateway-service-knoxidf/src/main/java/org/apache/knox/gateway/service/knoxidf/UserInfoResource.java b/gateway-service-knoxidf/src/main/java/org/apache/knox/gateway/service/knoxidf/UserInfoResource.java index 378b878ea1..e5bcb01e5a 100644 --- a/gateway-service-knoxidf/src/main/java/org/apache/knox/gateway/service/knoxidf/UserInfoResource.java +++ b/gateway-service-knoxidf/src/main/java/org/apache/knox/gateway/service/knoxidf/UserInfoResource.java @@ -42,6 +42,8 @@ import java.util.stream.Collectors; import static org.apache.knox.gateway.util.knoxidf.KnoxIDFConstants.BASE_RESORCE_PATH; +import static org.apache.knox.gateway.util.knoxidf.KnoxIDFConstants.SCOPE_ATTRIBUTE; +import static org.apache.knox.gateway.util.knoxidf.KnoxIDFConstants.TOKEN_ID_ATTRIBUTE; import static org.apache.knox.gateway.util.knoxidf.KnoxIDFUtils.error; @@ -82,12 +84,12 @@ public Response doPost() { @GET @Produces(MediaType.APPLICATION_JSON) public Response getUserInfo() throws UnknownTokenException, TokenServiceException { - final String tokenId = request.getAttribute("X-Token-Id") == null ? null : request.getAttribute("X-Token-Id").toString(); + final String tokenId = request.getAttribute(TOKEN_ID_ATTRIBUTE) == null ? null : request.getAttribute(TOKEN_ID_ATTRIBUTE).toString(); if (tokenId == null) { return error("invalid_request", "Cannot find tokenId"); } - final String scope = request.getAttribute("X-Token-Scope") == null ? "" : request.getAttribute("X-Token-Scope").toString(); + final String scope = request.getAttribute(SCOPE_ATTRIBUTE) == null ? "" : request.getAttribute(SCOPE_ATTRIBUTE).toString(); final TokenMetadata tokenMetadata = getReadonlyTokenStateService().getTokenMetadata(tokenId); final Map userInfo = new HashMap<>(); diff --git a/gateway-util-common/src/main/java/org/apache/knox/gateway/util/knoxidf/FederatedOpConfiguration.java b/gateway-util-common/src/main/java/org/apache/knox/gateway/util/knoxidf/FederatedOpConfiguration.java index 1ba8498a96..7ad6078433 100644 --- a/gateway-util-common/src/main/java/org/apache/knox/gateway/util/knoxidf/FederatedOpConfiguration.java +++ b/gateway-util-common/src/main/java/org/apache/knox/gateway/util/knoxidf/FederatedOpConfiguration.java @@ -18,7 +18,7 @@ import javax.servlet.ServletContext; -public class FederatedOpConfiguration implements Comparable { +public class FederatedOpConfiguration { private final boolean enabled; private final String name; private final String clientId; @@ -78,8 +78,4 @@ public String getDiscoveryEndpoint() { return discoveryEndpoint; } - @Override - public int compareTo(FederatedOpConfiguration other) { - return this.name.compareTo(other.name); - } } diff --git a/gateway-util-common/src/main/java/org/apache/knox/gateway/util/knoxidf/KnoxIDFConstants.java b/gateway-util-common/src/main/java/org/apache/knox/gateway/util/knoxidf/KnoxIDFConstants.java index b19c98f130..e3e895eb9b 100644 --- a/gateway-util-common/src/main/java/org/apache/knox/gateway/util/knoxidf/KnoxIDFConstants.java +++ b/gateway-util-common/src/main/java/org/apache/knox/gateway/util/knoxidf/KnoxIDFConstants.java @@ -23,7 +23,6 @@ public interface KnoxIDFConstants { String BASE_RESORCE_PATH = "knoxidf/api/v1"; String AUTH_CODE = "authorization_code"; - String CLIENT_SECRET = "client_secret"; String CLIENT_ID = "client_id"; String REDIRECT_URI = "redirect_uri"; String RESPONSE_TYPE = "response_type"; @@ -36,9 +35,11 @@ public interface KnoxIDFConstants { String CODE_RESPONSE_TYPE = RESPONSE_TYPE + "=" + CODE; String NONCE = "nonce"; + String TOKEN_ID_ATTRIBUTE = "X-Token-Id"; + String SCOPE_ATTRIBUTE = "X-Token-Scope"; + String FEDERATED_ID_TOKEN_PREFIX = "fed_id_"; String FEDERATED_ACCESS_TOKEN_PREFIX = "fed_access_"; String FEDERATED_OP_CONFIG_PREFIX = "federated.op."; - String FEDERATED_OP_CONFIG_ENABLED = FEDERATED_OP_CONFIG_PREFIX + "enabled"; String FEDERATED_OP_CONFIG_NAMES = FEDERATED_OP_CONFIG_PREFIX + "names"; } From 7e818b23da6129919998cb3ad1d6315db97ed716 Mon Sep 17 00:00:00 2001 From: Sandor Molnar Date: Tue, 28 Apr 2026 17:03:59 +0200 Subject: [PATCH 09/14] Removed unused imports --- .../federation/SSOCookieProviderTest.java | 37 +++++++++---------- .../security/token/JWTokenAttributes.java | 2 - 2 files changed, 17 insertions(+), 22 deletions(-) diff --git a/gateway-provider-security-jwt/src/test/java/org/apache/knox/gateway/provider/federation/SSOCookieProviderTest.java b/gateway-provider-security-jwt/src/test/java/org/apache/knox/gateway/provider/federation/SSOCookieProviderTest.java index 69df31383b..d96b2442ad 100644 --- a/gateway-provider-security-jwt/src/test/java/org/apache/knox/gateway/provider/federation/SSOCookieProviderTest.java +++ b/gateway-provider-security-jwt/src/test/java/org/apache/knox/gateway/provider/federation/SSOCookieProviderTest.java @@ -17,24 +17,7 @@ */ package org.apache.knox.gateway.provider.federation; -import static org.apache.knox.gateway.provider.federation.jwt.filter.SSOCookieFederationFilter.XHR_HEADER; -import static org.apache.knox.gateway.provider.federation.jwt.filter.SSOCookieFederationFilter.XHR_VALUE; -import static org.junit.Assert.fail; - -import java.io.UnsupportedEncodingException; -import java.nio.charset.StandardCharsets; -import java.security.Principal; -import java.time.Instant; -import java.util.Properties; -import java.util.Date; -import java.util.Set; -import java.util.concurrent.ThreadLocalRandom; - -import javax.servlet.ServletException; -import javax.servlet.http.Cookie; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - +import com.nimbusds.jwt.SignedJWT; import org.apache.knox.gateway.provider.federation.jwt.filter.AbstractJWTFilter; import org.apache.knox.gateway.provider.federation.jwt.filter.SSOCookieFederationFilter; import org.apache.knox.gateway.security.PrimaryPrincipal; @@ -45,11 +28,25 @@ import org.junit.Assert; import org.junit.Before; import org.junit.Test; - -import com.nimbusds.jwt.SignedJWT; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import javax.servlet.ServletException; +import javax.servlet.http.Cookie; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.nio.charset.StandardCharsets; +import java.security.Principal; +import java.time.Instant; +import java.util.Date; +import java.util.Properties; +import java.util.Set; +import java.util.concurrent.ThreadLocalRandom; + +import static org.apache.knox.gateway.provider.federation.jwt.filter.SSOCookieFederationFilter.XHR_HEADER; +import static org.apache.knox.gateway.provider.federation.jwt.filter.SSOCookieFederationFilter.XHR_VALUE; +import static org.junit.Assert.fail; + public class SSOCookieProviderTest extends AbstractJWTFilterTest { private static final Logger LOGGER = LoggerFactory.getLogger(SSOCookieProviderTest.class); diff --git a/gateway-spi/src/main/java/org/apache/knox/gateway/services/security/token/JWTokenAttributes.java b/gateway-spi/src/main/java/org/apache/knox/gateway/services/security/token/JWTokenAttributes.java index 2b12bf55e3..f5719ccc9d 100644 --- a/gateway-spi/src/main/java/org/apache/knox/gateway/services/security/token/JWTokenAttributes.java +++ b/gateway-spi/src/main/java/org/apache/knox/gateway/services/security/token/JWTokenAttributes.java @@ -19,9 +19,7 @@ import java.net.URI; import java.net.URISyntaxException; -import java.util.Collections; import java.util.Date; -import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; From b72fcfd026a5ea2eda461c762bf509fb8eaf70d7 Mon Sep 17 00:00:00 2001 From: Sandor Molnar Date: Tue, 5 May 2026 13:16:16 +0200 Subject: [PATCH 10/14] KnoxIDF - Add REFRESH_TOKEN support --- .../service/knoxidf/AuthorizeResource.java | 42 ++- .../service/knoxidf/DiscoveryResource.java | 3 +- .../service/knoxidf/RegistrationResource.java | 1 + .../service/knoxidf/TokenResource.java | 309 +++++++++++++----- .../service/knoxtoken/TokenResource.java | 18 +- .../security/token/TokenMetadataType.java | 2 +- .../services/security/token/TokenUtils.java | 17 + .../util/knoxidf/KnoxIDFConstants.java | 6 +- 8 files changed, 290 insertions(+), 108 deletions(-) diff --git a/gateway-service-knoxidf/src/main/java/org/apache/knox/gateway/service/knoxidf/AuthorizeResource.java b/gateway-service-knoxidf/src/main/java/org/apache/knox/gateway/service/knoxidf/AuthorizeResource.java index abc351cea7..82128ce75b 100644 --- a/gateway-service-knoxidf/src/main/java/org/apache/knox/gateway/service/knoxidf/AuthorizeResource.java +++ b/gateway-service-knoxidf/src/main/java/org/apache/knox/gateway/service/knoxidf/AuthorizeResource.java @@ -54,6 +54,7 @@ import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.ws.rs.GET; +import javax.ws.rs.POST; import javax.ws.rs.Path; import javax.ws.rs.QueryParam; import javax.ws.rs.core.Context; @@ -77,6 +78,7 @@ import static org.apache.knox.gateway.util.knoxidf.KnoxIDFConstants.BASE_RESORCE_PATH; import static org.apache.knox.gateway.util.knoxidf.KnoxIDFConstants.DEFAULT_SCOPES; +import static org.apache.knox.gateway.util.knoxidf.KnoxIDFConstants.OFFLINE_ACCESS_SCOPE; import static org.apache.knox.gateway.util.knoxidf.KnoxIDFUtils.error; @@ -109,13 +111,38 @@ public void init() throws ServletException, AliasServiceException, ServiceLifecy federatedIdentityService = services.getService(ServiceType.KNOXIDF_FEDERATED_IDENTITY_SERVICE); } + @Override @GET - public Response authorize(@QueryParam("response_type") String responseType, - @QueryParam("client_id") String clientId, - @QueryParam("redirect_uri") String redirectUri, - @QueryParam("scope") String scope, - @QueryParam("state") String state, - @QueryParam("nonce") String nonce) throws Exception { + public Response doGet() { + return authorize(); + } + + @Override + @POST + public Response doPost() { + return authorize(); + } + + public Response authorize() { + try { + return authorize( + request.getParameter("response_type"), + request.getParameter("client_id"), + request.getParameter("redirect_uri"), + request.getParameter("scope"), + request.getParameter("state"), + request.getParameter("nonce")); + } catch (Exception e) { + return error("server_error", e.getMessage()); + } + } + + public Response authorize(String responseType, + String clientId, + String redirectUri, + String scope, + String state, + String nonce) throws Exception { final String subject = SubjectUtils.getCurrentEffectivePrincipalName(); final Set requestedScopes = StringUtils.isBlank(scope) ? DEFAULT_SCOPES : new HashSet<>(Arrays.asList(scope.split("\\s+"))); final AuthorizeRequestMetadata authorizeRequestMetadata = new AuthorizeRequestMetadata(clientId, subject, responseType, redirectUri, requestedScopes, state, nonce); @@ -223,6 +250,9 @@ private void decorateAuthCodeToken(final String tokenId, final AuthorizeRequestM authCodeTokenMap.put("redirect_uri", authorizeRequestMetadata.getRedirectUri()); authCodeTokenMap.put("userName", authorizeRequestMetadata.getSubject()); authCodeTokenMap.put("scope", authorizeRequestMetadata.getJoinedRequestedScopes()); + if (authorizeRequestMetadata.getRequestedScopes().contains(OFFLINE_ACCESS_SCOPE)) { + authCodeTokenMap.put(OFFLINE_ACCESS_SCOPE, "true"); + } if (StringUtils.isNotBlank(authorizeRequestMetadata.getNonce())) { authCodeTokenMap.put("nonce", authorizeRequestMetadata.getNonce()); } diff --git a/gateway-service-knoxidf/src/main/java/org/apache/knox/gateway/service/knoxidf/DiscoveryResource.java b/gateway-service-knoxidf/src/main/java/org/apache/knox/gateway/service/knoxidf/DiscoveryResource.java index 603c8a25c6..def54ad2fe 100644 --- a/gateway-service-knoxidf/src/main/java/org/apache/knox/gateway/service/knoxidf/DiscoveryResource.java +++ b/gateway-service-knoxidf/src/main/java/org/apache/knox/gateway/service/knoxidf/DiscoveryResource.java @@ -65,7 +65,8 @@ public Response getConfig(@Context UriInfo uriInfo) { config.put("userinfo_endpoint", userInfoEndpoint); config.put("jwks_uri", baseUrl + JwksResource.RESOURCE_PATH); config.put("response_types_supported", new String[]{KnoxIDFConstants.CODE}); - config.put("grant_types_supported", new String[]{KnoxIDFConstants.AUTH_CODE}); + config.put("grant_types_supported", new String[]{KnoxIDFConstants.AUTH_CODE, KnoxIDFConstants.REFRESH_TOKEN}); + config.put("scopes_supported", KnoxIDFConstants.DEFAULT_SCOPES); config.put("id_token_signing_alg_values_supported", new String[]{"RS256"}); return Response.ok(JsonUtils.renderAsJsonString(config)).build(); } diff --git a/gateway-service-knoxidf/src/main/java/org/apache/knox/gateway/service/knoxidf/RegistrationResource.java b/gateway-service-knoxidf/src/main/java/org/apache/knox/gateway/service/knoxidf/RegistrationResource.java index 05675e4917..6be2ccace3 100644 --- a/gateway-service-knoxidf/src/main/java/org/apache/knox/gateway/service/knoxidf/RegistrationResource.java +++ b/gateway-service-knoxidf/src/main/java/org/apache/knox/gateway/service/knoxidf/RegistrationResource.java @@ -42,6 +42,7 @@ import static org.apache.knox.gateway.util.knoxidf.KnoxIDFConstants.BASE_RESORCE_PATH; import static org.apache.knox.gateway.util.knoxidf.KnoxIDFConstants.DEFAULT_SCOPES; +import static org.apache.knox.gateway.util.knoxidf.KnoxIDFConstants.OFFLINE_ACCESS_SCOPE; import static org.apache.knox.gateway.util.knoxidf.KnoxIDFUtils.error; @Path(RegistrationResource.RESOURCE_PATH) diff --git a/gateway-service-knoxidf/src/main/java/org/apache/knox/gateway/service/knoxidf/TokenResource.java b/gateway-service-knoxidf/src/main/java/org/apache/knox/gateway/service/knoxidf/TokenResource.java index d0c44fdddf..5507a17675 100644 --- a/gateway-service-knoxidf/src/main/java/org/apache/knox/gateway/service/knoxidf/TokenResource.java +++ b/gateway-service-knoxidf/src/main/java/org/apache/knox/gateway/service/knoxidf/TokenResource.java @@ -28,7 +28,9 @@ import org.apache.knox.gateway.services.security.token.JWTokenAttributesBuilder; import org.apache.knox.gateway.services.security.token.JWTokenAuthority; import org.apache.knox.gateway.services.security.token.TokenMetadata; +import org.apache.knox.gateway.services.security.token.TokenMetadataType; import org.apache.knox.gateway.services.security.token.TokenServiceException; +import org.apache.knox.gateway.services.security.token.TokenUtils; import org.apache.knox.gateway.services.security.token.UnknownTokenException; import org.apache.knox.gateway.services.security.token.impl.JWT; import org.apache.knox.gateway.util.ServletRequestUtils; @@ -43,10 +45,19 @@ import javax.ws.rs.core.Context; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; +import java.text.ParseException; import java.util.HashMap; import java.util.Map; +import static org.apache.knox.gateway.util.knoxidf.KnoxIDFConstants.AUTH_CODE; import static org.apache.knox.gateway.util.knoxidf.KnoxIDFConstants.BASE_RESORCE_PATH; +import static org.apache.knox.gateway.util.knoxidf.KnoxIDFConstants.CLIENT_ID; +import static org.apache.knox.gateway.util.knoxidf.KnoxIDFConstants.CODE; +import static org.apache.knox.gateway.util.knoxidf.KnoxIDFConstants.OFFLINE_ACCESS_SCOPE; +import static org.apache.knox.gateway.util.knoxidf.KnoxIDFConstants.REFRESH_TOKEN; +import static org.apache.knox.gateway.util.knoxidf.KnoxIDFConstants.REFRESH_TOKEN_TTL; +import static org.apache.knox.gateway.util.knoxidf.KnoxIDFConstants.REFRESH_TOKEN_TTL_DEFAULT; +import static org.apache.knox.gateway.util.knoxidf.KnoxIDFConstants.SCOPE; import static org.apache.knox.gateway.util.knoxidf.KnoxIDFUtils.error; @Path(TokenResource.RESOURCE_PATH) @@ -62,6 +73,7 @@ public class TokenResource extends PasscodeTokenResourceBase { private ServletContext servletContext; private FederatedIdentityService federatedIdentityService; + private long refreshTokenTTL; @Override public String getPrefix() { @@ -76,40 +88,38 @@ public void init() throws ServletException, AliasServiceException, ServiceLifecy this.userParamsProvider = new LdapUserParamsProvider(servletContext.getInitParameter("user.params.provider.ldap.url")); final GatewayServices services = (GatewayServices) servletContext.getAttribute(GatewayServices.GATEWAY_SERVICES_ATTRIBUTE); federatedIdentityService = services.getService(ServiceType.KNOXIDF_FEDERATED_IDENTITY_SERVICE); + setRefreshTokenTTL(); + } + + private void setRefreshTokenTTL() { + final String configuredRefreshTokenTTL = servletContext.getInitParameter(REFRESH_TOKEN_TTL); + if (StringUtils.isNotBlank(configuredRefreshTokenTTL)) { + this.refreshTokenTTL = Long.parseLong(configuredRefreshTokenTTL); + } else { + refreshTokenTTL = REFRESH_TOKEN_TTL_DEFAULT; + } } @Override @POST public Response doPost() { - final String code = getRequestParam("code"); - final String redirectUri = getRequestParam("redirect_uri"); - - final Response paramVerificationErrorResponse = verifyParams(code, redirectUri); - if (paramVerificationErrorResponse == null) { - try { - validateAuthCode(code, redirectUri); - return getAuthenticationToken(); - } catch (AuthTokenValidationError e) { - return error("Auth code validation error", e.getMessage()); - } finally { - try { - tokenStateService.revokeToken(code); - } catch (UnknownTokenException e) { - //NOP: this should have been handled by the above UnknownTokenException already - } - } + final String grantType = getRequestParam("grant_type"); + if (REFRESH_TOKEN.equals(grantType)) { + return handleRefreshToken(); + } else if (AUTH_CODE.equals(grantType)) { + return handleAuthorizationCodeFlow(); } - return paramVerificationErrorResponse; + return error("invalid_request", "invalid grant type: " + grantType); } @Override protected UserContext buildUserContext(HttpServletRequest request) { try { - final String code = getRequestParam("code"); + final String code = getRequestParam(CODE); final TokenMetadata tokenMetadata = tokenStateService.getTokenMetadata(code); - final String scope = tokenMetadata.getMetadata("scope"); + final String scope = tokenMetadata.getMetadata(SCOPE); final Map userParams = userParamsProvider.getParamsFor(tokenMetadata.getUserName(), scope); - userParams.put("scope", scope); + userParams.put(SCOPE, scope); return new UserContext(tokenMetadata.getUserName(), null, userParams); } catch (UnknownTokenException e) { //this should not happen as we have just validated the auth code @@ -121,14 +131,16 @@ protected UserContext buildUserContext(HttpServletRequest request) { protected void addArbitraryTokenMetadata(TokenMetadata tokenMetadata) { try { super.addArbitraryTokenMetadata(tokenMetadata); - final String code = getRequestParam("code"); - final TokenMetadata authCodeTokenMetadata = tokenStateService.getTokenMetadata(code); + final String code = getRequestParam(CODE); + if (StringUtils.isNotBlank(code)) { + final TokenMetadata authCodeTokenMetadata = tokenStateService.getTokenMetadata(code); - //if the auth code token was a result of a federated OIDC call, we need to save the associated - //federated identity ID in thw JWT too (so that it can be looked up while fetching user info) - final String federatedIdentityId = authCodeTokenMetadata.getMetadata("federated_identity_id"); - if (StringUtils.isNotBlank(federatedIdentityId)) { - tokenMetadata.add("federated_identity_id", federatedIdentityId); + //if the auth code token was a result of a federated OIDC call, we need to save the associated + //federated identity ID in thw JWT too (so that it can be looked up while fetching user info) + final String federatedIdentityId = authCodeTokenMetadata.getMetadata("federated_identity_id"); + if (StringUtils.isNotBlank(federatedIdentityId)) { + tokenMetadata.add("federated_identity_id", federatedIdentityId); + } } } catch (UnknownTokenException e) { //this should not happen as we have just validated the auth code @@ -139,44 +151,137 @@ protected void addArbitraryTokenMetadata(TokenMetadata tokenMetadata) { @Override protected ResponseMap buildResponseMap(JWT token, long expires) throws TokenServiceException { final ResponseMap responseMap = super.buildResponseMap(token, expires); - responseMap.map.put("id_token", generateIdToken(token)); + + final String code = getRequestParam(CODE); + TokenMetadata authCodeTokenMetadata = null; + if (StringUtils.isNotBlank(code)) { + try { + authCodeTokenMetadata = tokenStateService.getTokenMetadata(code); + } catch (UnknownTokenException e) { + //NOP + } + } + + responseMap.map.put("id_token", generateIdToken(token, authCodeTokenMetadata)); + + final String refreshToken = generateRefreshToken(token); + if (StringUtils.isNotBlank(refreshToken)) { + responseMap.map.put(REFRESH_TOKEN, refreshToken); + } + return responseMap; } - private String generateIdToken(JWT accessToken) throws TokenServiceException { + private Response handleRefreshToken() { try { - final String code = getRequestParam("code"); - final TokenMetadata tokenMetadata = tokenStateService.getTokenMetadata(code); - final boolean hasFederatedIdToken = StringUtils.isNotBlank(tokenMetadata.getMetadata("federated_identity_id")); + final String refreshTokenParam = getRequestParam(REFRESH_TOKEN); + final String refreshTokenId = TokenUtils.getTokenId(refreshTokenParam); + final TokenMetadata refreshTokenMetadata = tokenStateService.getTokenMetadata(refreshTokenId); + validateRefreshTokenGrant(refreshTokenParam, refreshTokenId, refreshTokenMetadata); + // Valid refresh token -> issue new access token and new refresh token (rotation) + final String userName = refreshTokenMetadata.getUserName(); + final String scope = refreshTokenMetadata.getMetadata(SCOPE); + final Map userParams = userParamsProvider.getParamsFor(userName, scope); + userParams.put(SCOPE, scope); + + // Revoke old refresh token (rotation) + tokenStateService.revokeToken(refreshTokenId); + + // Build new tokens + final UserContext userContext = new UserContext(userName, null, userParams); + final TokenResponseContext resp = getTokenResponse(userContext); + return resp.build(); + } catch (ParseException e) { + return error("invalid_grant", "Malformed refresh_token"); + } catch (UnknownTokenException e) { + return error("invalid_grant", "Unknown refresh_token"); + } catch (RefreshTokenValidationError e) { + return error("Refresh token validation error", e.getMessage()); + } + + } + + private void validateRefreshTokenGrant(String refreshTokenParam, String refreshTokenId, TokenMetadata refreshTokenMetadata) throws UnknownTokenException, RefreshTokenValidationError { + final String clientId = getRequestParam(CLIENT_ID); + + if (StringUtils.isBlank(refreshTokenParam)) { + throw new RefreshTokenValidationError("Invalid request: Missing refresh_token"); + } - if (hasFederatedIdToken) { - return generateFederatedIdToken(accessToken, tokenMetadata); + if (StringUtils.isBlank(clientId)) { + throw new RefreshTokenValidationError("Invalid request: Missing client_id"); + } + + if (refreshTokenMetadata == null || !TokenMetadataType.REFRESH_TOKEN.name().equals(refreshTokenMetadata.getType())) { + throw new RefreshTokenValidationError("Invalid grant: invalid refresh_token"); + } + + if (tokenStateService.getTokenExpiration(refreshTokenId) <= System.currentTimeMillis()) { + throw new RefreshTokenValidationError("Invalid grant: Refresh token expired"); + } + + final String associatedClientId = refreshTokenMetadata.getMetadata("client_id"); + if (!clientId.equals(associatedClientId)) { + throw new RefreshTokenValidationError("Invalid grant: client_id mismatch"); + } + } + + private Response handleAuthorizationCodeFlow() { + final String code = getRequestParam("code"); + final String redirectUri = getRequestParam("redirect_uri"); + + try { + validateAuthCode(code, redirectUri); + return getAuthenticationToken(); + } catch (AuthTokenValidationError e) { + return error("Auth code validation error", e.getMessage()); + } finally { + try { + tokenStateService.revokeToken(code); + } catch (UnknownTokenException e) { + //NOP: this should have been handled by the above UnknownTokenException already + } + } + } + + private void validateAuthCode(String code, String redirectUri) throws AuthTokenValidationError { + try { + if (code == null || code.isEmpty()) { + throw new AuthTokenValidationError("Invalid request: missing code"); + } + + if (redirectUri == null || redirectUri.isEmpty()) { + throw new AuthTokenValidationError("Invalid request: missing redirect_uri"); + } + + final TokenMetadata authCodeTokenMetadata = tokenStateService.getTokenMetadata(code); + final String associateRedirectUri = authCodeTokenMetadata.getMetadata("redirect_uri"); + if (!authCodeTokenMetadata.isAuthCode()) { + throw new AuthTokenValidationError("Invalid auth_code: not an auth code token"); + } else if (tokenStateService.getTokenExpiration(code) <= System.currentTimeMillis()) { + throw new AuthTokenValidationError("Invalid auth_code: expired"); + } else if (!associateRedirectUri.equals(redirectUri)) { + throw new AuthTokenValidationError("Invalid redirect_uri: " + redirectUri); } else { - return generateLocalIdToken(accessToken, tokenMetadata); + final String associatedClientId = authCodeTokenMetadata.getMetadata("client_id"); + final String clientId = getRequestParam("client_id"); + if (!associatedClientId.equals(clientId)) { + throw new AuthTokenValidationError("Invalid client_id: " + clientId); + } } } catch (UnknownTokenException e) { - return null; //should not happen + throw new AuthTokenValidationError("Unknown auth_code"); } } - private String generateLocalIdToken(JWT accessToken, TokenMetadata tokenMetadata) throws TokenServiceException { - final JWTokenAttributesBuilder idTokenAttributesBuilder = new JWTokenAttributesBuilder(); - idTokenAttributesBuilder - .setAlgorithm(accessToken.getSignatureAlgorithm().getName()) - .setUserName(accessToken.getSubject()) - .setIssueTime(System.currentTimeMillis()) - .setExpires(Long.parseLong(accessToken.getExpires())) - .setIssuer(accessToken.getIssuer()); - final String associatedClientId = tokenMetadata.getMetadata("client_id"); - idTokenAttributesBuilder.setAudiences(associatedClientId); - final String nonce = tokenMetadata.getMetadata("nonce"); - if (StringUtils.isNotBlank(nonce)) { - idTokenAttributesBuilder.setCustomAttributes(Map.of("nonce", nonce)); - } + private String generateIdToken(JWT accessToken, TokenMetadata authCodeTokenMetadata) throws TokenServiceException { + final boolean hasFederatedIdToken = authCodeTokenMetadata != null && StringUtils.isNotBlank(authCodeTokenMetadata.getMetadata("federated_identity_id")); - final JWTokenAuthority ts = getGatewayServices().getService(ServiceType.TOKEN_SERVICE); - final JWT idToken = ts.issueToken(idTokenAttributesBuilder.build()); - return idToken.toString(); + if (hasFederatedIdToken) { + return generateFederatedIdToken(accessToken, authCodeTokenMetadata); + } else { + return generateLocalIdToken(accessToken, authCodeTokenMetadata); + } } private String generateFederatedIdToken(JWT accessToken, TokenMetadata tokenMetadata) throws TokenServiceException { @@ -207,42 +312,76 @@ private String generateFederatedIdToken(JWT accessToken, TokenMetadata tokenMeta builder.setCustomAttributes(claims); - JWTokenAuthority ts = getGatewayServices().getService(ServiceType.TOKEN_SERVICE); - return ts.issueToken(builder.build()).toString(); + return issueToken(builder).toString(); } - private Response verifyParams(String code, String redirectUri) { - if (code == null || code.isEmpty()) { - return error("invalid_request", "Missing code"); - } + private String generateLocalIdToken(JWT accessToken, TokenMetadata authCodeTokenMetadata) throws TokenServiceException { + final JWTokenAttributesBuilder idTokenAttributesBuilder = new JWTokenAttributesBuilder(); + idTokenAttributesBuilder + .setAlgorithm(accessToken.getSignatureAlgorithm().getName()) + .setUserName(accessToken.getSubject()) + .setIssueTime(System.currentTimeMillis()) + .setExpires(Long.parseLong(accessToken.getExpires())) + .setIssuer(accessToken.getIssuer()); - if (redirectUri == null || redirectUri.isEmpty()) { - return error("invalid_request", "Missing redirect_uri"); + if (authCodeTokenMetadata != null) { + final String associatedClientId = authCodeTokenMetadata.getMetadata("client_id"); + idTokenAttributesBuilder.setAudiences(associatedClientId); + final String nonce = authCodeTokenMetadata.getMetadata("nonce"); + if (StringUtils.isNotBlank(nonce)) { + idTokenAttributesBuilder.setCustomAttributes(Map.of("nonce", nonce)); + } + } else { + // If there is no auth code (e.g. refresh token grant), we use the client_id from the request + idTokenAttributesBuilder.setAudiences(getRequestParam("client_id")); } - return null; + return issueToken(idTokenAttributesBuilder).toString(); } - private void validateAuthCode(String code, String redirectUri) throws AuthTokenValidationError { - try { - final TokenMetadata tokenMetadata = tokenStateService.getTokenMetadata(code); - final String associatedClientId = tokenMetadata.getMetadata("client_id"); - final String associateRedirectUri = tokenMetadata.getMetadata("redirect_uri"); - if (!tokenMetadata.isAuthCode()) { - throw new AuthTokenValidationError("Invalid auth_code: not an auth code token"); //this one or the previous one might be redundant - } else if (tokenStateService.getTokenExpiration(code) <= System.currentTimeMillis()) { - throw new AuthTokenValidationError("Invalid auth_code: expired"); - } else if (!associateRedirectUri.equals(redirectUri)) { - throw new AuthTokenValidationError("Invalid redirect_uri: " + redirectUri); - } else { - final String clientId = getRequestParam("client_id"); - if (!associatedClientId.equals(clientId)) { - throw new AuthTokenValidationError("Invalid client_id: " + clientId); - } - } - } catch (UnknownTokenException e) { - throw new AuthTokenValidationError("Unknown auth_code"); + private String generateRefreshToken(JWT accessToken) throws TokenServiceException { + final String scope = (String) accessToken.getJWTClaimsSet().getClaim("scope"); + if (StringUtils.isNotBlank(scope) && scope.contains(OFFLINE_ACCESS_SCOPE)) { + return issueRefreshToken(accessToken, scope); + } else { + return null; + } + } + + private String issueRefreshToken(JWT accessToken, String scope) throws TokenServiceException { + final JWTokenAttributesBuilder refreshTokenAttributesBuilder = new JWTokenAttributesBuilder(); + + final long issueTime = System.currentTimeMillis(); + final long expires = issueTime + refreshTokenTTL; + final String clientId = getRequestParam("client_id"); + + refreshTokenAttributesBuilder.setIssuer(accessToken.getIssuer()) + .setUserName(accessToken.getSubject()) + .setAlgorithm(accessToken.getSignatureAlgorithm().getName()) + .setAudiences(clientId) + .setIssueTime(issueTime) + .setExpires(expires) + .setManaged(tokenStateService != null) + .setType(TokenMetadataType.REFRESH_TOKEN.name()); + + final JWT refreshToken = issueToken(refreshTokenAttributesBuilder); + + if (tokenStateService != null) { + final String tokenId = TokenUtils.getTokenId(refreshToken); + tokenStateService.addToken(tokenId, issueTime, expires, tokenStateService.getDefaultMaxLifetimeDuration()); + final TokenMetadata metadata = new TokenMetadata(refreshToken.getSubject()); + metadata.setType(TokenMetadataType.REFRESH_TOKEN); + metadata.add("client_id", clientId); + metadata.add("scope", scope); + tokenStateService.addMetadata(tokenId, metadata); } + + return refreshToken.toString(); + } + + private JWT issueToken(final JWTokenAttributesBuilder builder) throws TokenServiceException { + final JWTokenAuthority ts = getGatewayServices().getService(ServiceType.TOKEN_SERVICE); + return ts.issueToken(builder.build()); } private String getRequestParam(String paramName) { @@ -258,4 +397,10 @@ private static class AuthTokenValidationError extends Exception { super(message); } } + + private static class RefreshTokenValidationError extends Exception { + RefreshTokenValidationError(String message) { + super(message); + } + } } diff --git a/gateway-service-knoxtoken/src/main/java/org/apache/knox/gateway/service/knoxtoken/TokenResource.java b/gateway-service-knoxtoken/src/main/java/org/apache/knox/gateway/service/knoxtoken/TokenResource.java index f7dc60c9e5..9dd26ba444 100644 --- a/gateway-service-knoxtoken/src/main/java/org/apache/knox/gateway/service/knoxtoken/TokenResource.java +++ b/gateway-service-knoxtoken/src/main/java/org/apache/knox/gateway/service/knoxtoken/TokenResource.java @@ -694,7 +694,7 @@ public Response revoke(String token) { } else { try { final String revoker = SubjectUtils.getCurrentEffectivePrincipalName(); - final String tokenId = getTokenId(token); + final String tokenId = TokenUtils.getTokenId(token); if (isKnoxSsoCookie(tokenId)) { errorStatus = Response.Status.FORBIDDEN; error = "SSO cookie (" + Tokens.getTokenIDDisplayText(tokenId) + ") cannot not be revoked."; @@ -746,22 +746,6 @@ private boolean triesToRevokeOwnToken(String tokenId, String revoker) throws Unk return StringUtils.isNotBlank(revoker) && (revoker.equals(tokenUserName) || revoker.equals(tokenCreatedBy)); } - /* - * If the supplied 'token' conforms the UUID string representation, we consider - * that as the token ID; otherwise we expect that 'token' is the entire JWT and - * we get the token ID from it - */ - private String getTokenId(String token) throws ParseException { - try { - UUID.fromString(token); - return token; - } catch (IllegalArgumentException e) { - //NOP: the supplied token is not a UUID, we expect the entire JWT - } - final JWTToken jwt = new JWTToken(token); - return TokenUtils.getTokenId(jwt); - } - @PUT @Path(ENABLE_PATH) @Produces({APPLICATION_JSON}) diff --git a/gateway-spi/src/main/java/org/apache/knox/gateway/services/security/token/TokenMetadataType.java b/gateway-spi/src/main/java/org/apache/knox/gateway/services/security/token/TokenMetadataType.java index 8a13bf1d48..4d0080fd57 100644 --- a/gateway-spi/src/main/java/org/apache/knox/gateway/services/security/token/TokenMetadataType.java +++ b/gateway-spi/src/main/java/org/apache/knox/gateway/services/security/token/TokenMetadataType.java @@ -18,6 +18,6 @@ public enum TokenMetadataType { - JWT, KNOXSSO_COOKIE, CLIENT_ID, API_KEY, AUTH_CODE; + JWT, KNOXSSO_COOKIE, CLIENT_ID, API_KEY, AUTH_CODE, REFRESH_TOKEN; } diff --git a/gateway-spi/src/main/java/org/apache/knox/gateway/services/security/token/TokenUtils.java b/gateway-spi/src/main/java/org/apache/knox/gateway/services/security/token/TokenUtils.java index e9620a4258..73d84f96b9 100644 --- a/gateway-spi/src/main/java/org/apache/knox/gateway/services/security/token/TokenUtils.java +++ b/gateway-spi/src/main/java/org/apache/knox/gateway/services/security/token/TokenUtils.java @@ -30,7 +30,9 @@ import javax.servlet.FilterConfig; import javax.servlet.ServletContext; import java.security.interfaces.RSAPublicKey; +import java.text.ParseException; import java.util.LinkedHashMap; +import java.util.UUID; public class TokenUtils { public static final String ATTR_CURRENT_KNOXSSO_COOKIE_TOKEN_ID = "currentKnoxSsoCookieTokenId"; @@ -49,6 +51,21 @@ public static String getTokenId(final JWT token) { return token.getClaim(JWTToken.KNOX_ID_CLAIM); } + /** + * If the supplied 'token' conforms the UUID string representation, we consider + * that as the token ID; otherwise we expect that 'token' is the entire JWT, and + * we get the token ID from it + */ + public static String getTokenId(String token) throws ParseException { + try { + UUID.fromString(token); + return token; + } catch (IllegalArgumentException e) { + //NOP: the supplied token is not a UUID, we expect the entire JWT + } + return getTokenId(new JWTToken(token)); + } + /** * Determine if server-managed token state is enabled for a provider, based on configuration. * The analysis includes checking the provider params and the gateway configuration. diff --git a/gateway-util-common/src/main/java/org/apache/knox/gateway/util/knoxidf/KnoxIDFConstants.java b/gateway-util-common/src/main/java/org/apache/knox/gateway/util/knoxidf/KnoxIDFConstants.java index e3e895eb9b..6526a54e3d 100644 --- a/gateway-util-common/src/main/java/org/apache/knox/gateway/util/knoxidf/KnoxIDFConstants.java +++ b/gateway-util-common/src/main/java/org/apache/knox/gateway/util/knoxidf/KnoxIDFConstants.java @@ -28,10 +28,14 @@ public interface KnoxIDFConstants { String RESPONSE_TYPE = "response_type"; Set ALLOWED_RESPONSE_TYPES = Sets.newHashSet("code", "id_token", "code id_token"); String SCOPE = "scope"; - Set DEFAULT_SCOPES = Sets.newHashSet("openid", "profile", "email"); + String OFFLINE_ACCESS_SCOPE = "offline_access"; + Set DEFAULT_SCOPES = Sets.newHashSet("openid", "profile", "email", OFFLINE_ACCESS_SCOPE); String OPENID_SCOPE = SCOPE + "=openid"; String STATE = "state"; String CODE = "code"; + String REFRESH_TOKEN = "refresh_token"; + String REFRESH_TOKEN_TTL= "refresh.token.ttl"; + long REFRESH_TOKEN_TTL_DEFAULT = 86400000L; // 1 day String CODE_RESPONSE_TYPE = RESPONSE_TYPE + "=" + CODE; String NONCE = "nonce"; From 3027087cd7f8083132c62c5fab66e33618637610 Mon Sep 17 00:00:00 2001 From: Sandor Molnar Date: Tue, 5 May 2026 14:07:19 +0200 Subject: [PATCH 11/14] KnoxIDF - Automatically enable JdbcFederatedIdentityService when KnoxIDF is present in any topology --- .../FederatedIdentityServiceFactory.java | 33 ++++++++++++++++--- .../service/knoxidf/AuthorizeResource.java | 1 - .../service/knoxidf/RegistrationResource.java | 1 - 3 files changed, 28 insertions(+), 7 deletions(-) diff --git a/gateway-server/src/main/java/org/apache/knox/gateway/services/factory/FederatedIdentityServiceFactory.java b/gateway-server/src/main/java/org/apache/knox/gateway/services/factory/FederatedIdentityServiceFactory.java index e0107b672d..f78f96af6d 100644 --- a/gateway-server/src/main/java/org/apache/knox/gateway/services/factory/FederatedIdentityServiceFactory.java +++ b/gateway-server/src/main/java/org/apache/knox/gateway/services/factory/FederatedIdentityServiceFactory.java @@ -26,6 +26,8 @@ import org.apache.knox.gateway.services.knoxidf.federation.EmptyFederatedIdentitityService; import org.apache.knox.gateway.services.knoxidf.federation.FederatedIdentityService; import org.apache.knox.gateway.services.knoxidf.federation.JdbcFederatedIdentityService; +import org.apache.knox.gateway.services.topology.TopologyService; +import org.apache.knox.gateway.topology.Topology; import java.util.Collection; import java.util.List; @@ -39,29 +41,50 @@ public class FederatedIdentityServiceFactory extends AbstractServiceFactory { @Override protected Service createService(GatewayServices gatewayServices, ServiceType serviceType, GatewayConfig gatewayConfig, Map options, String implementation) throws ServiceLifecycleException { + + String implementationToUse = implementation; + // If implementation is empty, check if we should auto-enable JdbcFederatedIdentityService + if (isEmptyDefaultImplementation(implementationToUse)) { + if (isKnoxIdfEnabledInAnyTopology(gatewayServices)) { + implementationToUse = JdbcFederatedIdentityService.class.getName(); + } + } + FederatedIdentityService service = null; - if (shouldCreateService(implementation)) { - if (matchesImplementation(implementation, EmptyFederatedIdentitityService.class, true)) { + if (shouldCreateService(implementationToUse)) { + if (matchesImplementation(implementationToUse, EmptyFederatedIdentitityService.class, true)) { service = new EmptyFederatedIdentitityService(); - } else if (matchesImplementation(implementation, JdbcFederatedIdentityService.class)) { + } else if (matchesImplementation(implementationToUse, JdbcFederatedIdentityService.class)) { try { try { service = new JdbcFederatedIdentityService(); ((JdbcFederatedIdentityService) service).setAliasService(getAliasService(gatewayServices)); service.init(gatewayConfig, options); } catch (ServiceLifecycleException e) { - LOG.errorInitializingService(implementation, e.getMessage(), e); + LOG.errorInitializingService(implementationToUse, e.getMessage(), e); service = new EmptyFederatedIdentitityService(); } } catch (Exception e) { throw new ServiceLifecycleException("Error while creating Federated Identity Service: " + e, e); } } - logServiceUsage(implementation, serviceType); + logServiceUsage(service.getClass().getName(), serviceType); } return service; } + private boolean isKnoxIdfEnabledInAnyTopology(GatewayServices gatewayServices) { + final TopologyService topologyService = gatewayServices.getService(ServiceType.TOPOLOGY_SERVICE); + if (topologyService != null) { + for (Topology topology : topologyService.getTopologies()) { + if (topology.getServices().stream().anyMatch(service -> "KNOXIDF".equals(service.getRole()))) { + return true; + } + } + } + return false; + } + @Override protected ServiceType getServiceType() { return ServiceType.KNOXIDF_FEDERATED_IDENTITY_SERVICE; diff --git a/gateway-service-knoxidf/src/main/java/org/apache/knox/gateway/service/knoxidf/AuthorizeResource.java b/gateway-service-knoxidf/src/main/java/org/apache/knox/gateway/service/knoxidf/AuthorizeResource.java index 82128ce75b..a49369ee4e 100644 --- a/gateway-service-knoxidf/src/main/java/org/apache/knox/gateway/service/knoxidf/AuthorizeResource.java +++ b/gateway-service-knoxidf/src/main/java/org/apache/knox/gateway/service/knoxidf/AuthorizeResource.java @@ -56,7 +56,6 @@ import javax.ws.rs.GET; import javax.ws.rs.POST; import javax.ws.rs.Path; -import javax.ws.rs.QueryParam; import javax.ws.rs.core.Context; import javax.ws.rs.core.Response; import java.io.UnsupportedEncodingException; diff --git a/gateway-service-knoxidf/src/main/java/org/apache/knox/gateway/service/knoxidf/RegistrationResource.java b/gateway-service-knoxidf/src/main/java/org/apache/knox/gateway/service/knoxidf/RegistrationResource.java index 6be2ccace3..05675e4917 100644 --- a/gateway-service-knoxidf/src/main/java/org/apache/knox/gateway/service/knoxidf/RegistrationResource.java +++ b/gateway-service-knoxidf/src/main/java/org/apache/knox/gateway/service/knoxidf/RegistrationResource.java @@ -42,7 +42,6 @@ import static org.apache.knox.gateway.util.knoxidf.KnoxIDFConstants.BASE_RESORCE_PATH; import static org.apache.knox.gateway.util.knoxidf.KnoxIDFConstants.DEFAULT_SCOPES; -import static org.apache.knox.gateway.util.knoxidf.KnoxIDFConstants.OFFLINE_ACCESS_SCOPE; import static org.apache.knox.gateway.util.knoxidf.KnoxIDFUtils.error; @Path(RegistrationResource.RESOURCE_PATH) From 8a12ee4be10ef6ebcabe965f613dbf2ec9c1052f Mon Sep 17 00:00:00 2001 From: Sandor Molnar Date: Wed, 6 May 2026 14:41:53 +0200 Subject: [PATCH 12/14] KnoxIDF - Added Docker-based integration tests --- .github/workflows/build/Dockerfile | 2 + .github/workflows/build/Dockerfile.local | 2 + .../build/conf/topologies/knoxidf-ldap.xml | 71 ++++++ .../build/conf/topologies/knoxidf-token.xml | 46 ++++ .github/workflows/compose/docker-compose.yml | 4 +- .github/workflows/tests.yml | 22 ++ .github/workflows/tests/common_utils.py | 31 +++ .github/workflows/tests/test_knoxidf.py | 207 ++++++++++++++++++ .../gateway/util/knoxidf/KnoxIDFUtils.java | 8 +- 9 files changed, 387 insertions(+), 6 deletions(-) create mode 100644 .github/workflows/build/conf/topologies/knoxidf-ldap.xml create mode 100644 .github/workflows/build/conf/topologies/knoxidf-token.xml create mode 100644 .github/workflows/tests/test_knoxidf.py diff --git a/.github/workflows/build/Dockerfile b/.github/workflows/build/Dockerfile index 441cf1ba43..7b9f42de8d 100644 --- a/.github/workflows/build/Dockerfile +++ b/.github/workflows/build/Dockerfile @@ -38,6 +38,8 @@ ADD gateway-site.xml /knox-runtime/conf/gateway-site.xml ADD conf/topologies/knoxtoken.xml /knox-runtime/conf/topologies/knoxtoken.xml ADD conf/topologies/knoxldap.xml /knox-runtime/conf/topologies/knoxldap.xml ADD conf/topologies/remoteauth.xml /knox-runtime/conf/topologies/remoteauth.xml +ADD conf/topologies/knoxidf-ldap.xml /knox-runtime/conf/topologies/knoxidf-ldap.xml +ADD conf/topologies/knoxidf-token.xml /knox-runtime/conf/topologies/knoxidf-token.xml ADD conf/topologies/health.xml /knox-runtime/conf/topologies/health.xml diff --git a/.github/workflows/build/Dockerfile.local b/.github/workflows/build/Dockerfile.local index 73e1c9bfe8..d2ff6815da 100644 --- a/.github/workflows/build/Dockerfile.local +++ b/.github/workflows/build/Dockerfile.local @@ -60,6 +60,8 @@ ADD conf/topologies/knoxtoken.xml /knox-runtime/conf/topologies/knoxtoken.xml ADD conf/topologies/health.xml /knox-runtime/conf/topologies/health.xml ADD conf/topologies/knoxldap.xml /knox-runtime/conf/topologies/knoxldap.xml ADD conf/topologies/remoteauth.xml /knox-runtime/conf/topologies/remoteauth.xml +ADD conf/topologies/knoxidf-ldap.xml /knox-runtime/conf/topologies/knoxidf-ldap.xml +ADD conf/topologies/knoxidf-token.xml /knox-runtime/conf/topologies/knoxidf-token.xml RUN chown -R gateway /knox-runtime/ diff --git a/.github/workflows/build/conf/topologies/knoxidf-ldap.xml b/.github/workflows/build/conf/topologies/knoxidf-ldap.xml new file mode 100644 index 0000000000..b0c340f6b1 --- /dev/null +++ b/.github/workflows/build/conf/topologies/knoxidf-ldap.xml @@ -0,0 +1,71 @@ + + + + + authentication + ShiroProvider + true + + main.ldapRealm + org.apache.knox.gateway.shirorealm.KnoxLdapRealm + + + main.ldapRealm.userDnTemplate + uid={0},ou=people,dc=hadoop,dc=apache,dc=org + + + main.ldapRealm.contextFactory.url + ldap://ldap:33389 + + + main.ldapRealm.contextFactory.authenticationMechanism + simple + + + urls./knoxidf/api/v1/.well-known/openid-configuration + anon + + + urls./knoxidf/api/v1/client/register + anon + + + urls./knoxidf/api/v1/authorize/callback + anon + + + urls./knoxidf/api/v1/jwks + anon + + + urls./** + authcBasic + + + + identity-assertion + Default + true + + + + + KNOXIDF + + knoxidf.knox.token.ttl + 60000 + + + knoxidf.knox.token.limit.per.user + -1 + + + token.exchange.topology.name + knoxidf-token + + + user.params.provider.ldap.url + ldap://ldap:33389 + + + diff --git a/.github/workflows/build/conf/topologies/knoxidf-token.xml b/.github/workflows/build/conf/topologies/knoxidf-token.xml new file mode 100644 index 0000000000..002c3813d2 --- /dev/null +++ b/.github/workflows/build/conf/topologies/knoxidf-token.xml @@ -0,0 +1,46 @@ + + + + + federation + JWTProvider + true + + knox.token.exp.server-managed + true + + + + identity-assertion + Default + true + + + + + KNOXIDF + + knoxidf.knox.token.ttl + 86400000 + + + knoxidf.knox.token.limit.per.user + -1 + + + user.params.provider.ldap.url + ldap://ldap:33389 + + + + KNOXTOKEN + + knox.token.ttl + 60000 + + + knox.token.limit.per.user + -1 + + + diff --git a/.github/workflows/compose/docker-compose.yml b/.github/workflows/compose/docker-compose.yml index 3aa1798edc..eab1382f23 100644 --- a/.github/workflows/compose/docker-compose.yml +++ b/.github/workflows/compose/docker-compose.yml @@ -25,8 +25,8 @@ services: context: ../build dockerfile: Dockerfile.local args: - knoxurl: ${knoxurl:-https://github.com/apache/knox.git} - branch: ${branch:-master} + knoxurl: ${knoxurl:-https://github.com/smolnar82/knox.git} + branch: ${branch:-knox_idf_smolnar} image: apache/knox-dev:local-${GITHUB_RUN_ID:-local}-${GITHUB_RUN_ID:-local} ldap: diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 4f90c4a379..7cd9e992d4 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -82,6 +82,14 @@ jobs: # Run the tests service defined in docker-compose.yml docker compose -f ./.github/workflows/compose/docker-compose.yml up --exit-code-from tests tests + - name: Collect Knox Logs and Conf + if: always() + run: | + mkdir -p .github/workflows/artifacts/knox-logs + mkdir -p .github/workflows/artifacts/knox-conf + docker compose -f ./.github/workflows/compose/docker-compose.yml cp knox:/knox-runtime/logs .github/workflows/artifacts/knox-logs + docker compose -f ./.github/workflows/compose/docker-compose.yml cp knox:/knox-runtime/conf .github/workflows/artifacts/knox-conf + - name: Upload Test Results if: (!cancelled()) uses: actions/upload-artifact@v4 @@ -89,6 +97,20 @@ jobs: name: test-results path: .github/workflows/tests/test-results.xml + - name: Upload Knox Logs + if: always() + uses: actions/upload-artifact@v4 + with: + name: knox-logs + path: .github/workflows/artifacts/knox-logs + + - name: Upload Knox Conf + if: always() + uses: actions/upload-artifact@v4 + with: + name: knox-conf + path: .github/workflows/artifacts/knox-conf + - name: Upload Event File uses: actions/upload-artifact@v4 with: diff --git a/.github/workflows/tests/common_utils.py b/.github/workflows/tests/common_utils.py index 41198b09e1..0b544b4e98 100644 --- a/.github/workflows/tests/common_utils.py +++ b/.github/workflows/tests/common_utils.py @@ -15,6 +15,8 @@ from __future__ import annotations +import base64 +import json import os import unittest from typing import Any @@ -66,3 +68,32 @@ def collect_actor_group_values( def assert_hsts_header(testcase: unittest.TestCase, response: requests.Response) -> None: testcase.assertIn(HSTS_HEADER_NAME, response.headers) testcase.assertEqual(response.headers[HSTS_HEADER_NAME], HSTS_EXPECTED_VALUE) + +def get_token_id_display_text(uuid): + """ + Format the token ID for display, matching Knox's getTokenIDDisplayText logic. + """ + if uuid and len(uuid) == 36 and "-" in uuid: + first_dash = uuid.find('-') + last_dash = uuid.rfind('-') + return f"{uuid[:first_dash]}...{uuid[last_dash+1:]}" + return uuid + + +def get_token_claim(token, claim): + """ + Decodes a JWT token and returns the value of the specified claim. + """ + try: + payload_b64 = token.split('.')[1] + # URL-safe base64 decoding usually needs padding adjustment + missing_padding = len(payload_b64) % 4 + if missing_padding: + payload_b64 += '=' * (4 - missing_padding) + # Use urlsafe_b64decode just in case, though standard b64decode often works with padding + payload_json = base64.urlsafe_b64decode(payload_b64).decode('utf-8') + payload = json.loads(payload_json) + return payload.get(claim) + except Exception as e: + print(f"Failed to decode token for claim '{claim}': {e}") + return None \ No newline at end of file diff --git a/.github/workflows/tests/test_knoxidf.py b/.github/workflows/tests/test_knoxidf.py new file mode 100644 index 0000000000..93825fba5d --- /dev/null +++ b/.github/workflows/tests/test_knoxidf.py @@ -0,0 +1,207 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to you under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import unittest +from urllib.parse import urlparse, parse_qs +from requests.auth import HTTPBasicAuth + +from common_utils import gateway_base_url, knox_get, knox_post, get_token_claim, get_token_id_display_text + +class TestKnoxIDF(unittest.TestCase): + def setUp(self): + # Get the Knox Gateway URL from environment variables + self.base_url = gateway_base_url() + self.knoxidf_ldap_url = f"{self.base_url}gateway/knoxidf-ldap/" + self.knoxidf_token_url = f"{self.base_url}gateway/knoxidf-token/" + self.username = "guest" + self.password = "guest-password" + + def test_discovery(self): + """ + Test OIDC Discovery endpoint. + """ + url = f"{self.knoxidf_ldap_url}knoxidf/api/v1/.well-known/openid-configuration" + print(f"Testing Discovery URL: {url}") + response = knox_get(url) + self.assertEqual(response.status_code, 200) + config = response.json() + + # Construct expected values based on dynamic base_url + expected_issuer = f"{self.knoxidf_ldap_url}knoxidf" + expected_auth_endpoint = f"{self.knoxidf_ldap_url}knoxidf/api/v1/authorize" + expected_token_endpoint = f"{self.knoxidf_token_url}knoxidf/api/v1/token" + expected_userinfo_endpoint = f"{self.knoxidf_token_url}knoxidf/api/v1/userinfo" + expected_jwks_uri = f"{self.knoxidf_ldap_url}knoxidf/api/v1/jwks" + + self.assertEqual(config.get("issuer"), expected_issuer) + self.assertEqual(config.get("authorization_endpoint"), expected_auth_endpoint) + self.assertEqual(config.get("token_endpoint"), expected_token_endpoint) + self.assertEqual(config.get("userinfo_endpoint"), expected_userinfo_endpoint) + self.assertEqual(config.get("jwks_uri"), expected_jwks_uri) + + self.assertEqual(config.get("response_types_supported"), ["code"]) + self.assertEqual(config.get("grant_types_supported"), ["authorization_code", "refresh_token"]) + self.assertEqual(config.get("id_token_signing_alg_values_supported"), ["RS256"]) + self.assertEqual(config.get("scopes_supported"), ["openid", "email", "profile", "offline_access"]) + + def test_client_credentials_flow(self): + """ + Test OIDC Client Credentials Flow. + """ + # 1. Register client + reg_url = f"{self.knoxidf_ldap_url}knoxidf/api/v1/client/register" + print(f"Registering client at: {reg_url}") + data = { + "redirect_uris": "http://localhost/callback", + "allowed_scopes": "openid,profile,email,offline_access" + } + response = knox_post( + reg_url, + data=data, + auth=HTTPBasicAuth(self.username, self.password), + ) + self.assertEqual(response.status_code, 200) + reg_info = response.json() + print(f"Registration response: {reg_info}") + client_id = reg_info["client_id"] + client_secret = reg_info["client_secret"] + + # 2. Get token via client_credentials + token_url = f"{self.knoxidf_token_url}knoxtoken/api/v1/token" + print(f"Getting token at: {token_url}") + data = { + "grant_type": "client_credentials", + "scope": "openid", + "client_id": client_id, + "client_secret": client_secret + } + # ClientCredentialsResource uses Basic Auth for client authentication + response = knox_post(token_url, data=data, verify=False) + if response.status_code != 200: + print(f"Token error response: {response.text}") + self.assertEqual(response.status_code, 200) + tokens = response.json() + self.assertIn("access_token", tokens) + self.assertEqual(tokens["token_type"], "Bearer") + + def test_authorization_code_flow(self): + """ + Test OIDC Authorization Code Flow with Refresh Token. + """ + # 1. Register client + reg_url = f"{self.knoxidf_ldap_url}knoxidf/api/v1/client/register" + print(f"Registering client at: {reg_url}") + data = { + "redirect_uris": "http://localhost/callback", + "allowed_scopes": "openid,profile,email,offline_access" + } + response = knox_post( + reg_url, + data=data, + auth=HTTPBasicAuth(self.username, self.password), + ) + self.assertEqual(response.status_code, 200) + reg_info = response.json() + print(f"Registration response: {reg_info}") + client_id = reg_info["client_id"] + client_secret = reg_info["client_secret"] + + # 2. Authorize (with Basic Auth for the user 'guest') + auth_url = f"{self.knoxidf_ldap_url}knoxidf/api/v1/authorize" + params = { + "response_type": "code", + "client_id": client_id, + "redirect_uri": "http://localhost/callback", + "scope": "openid offline_access", + "state": "test_state", + "auto_consent": "true" + } + print(f"Authorizing at: {auth_url}") + # allow_redirects=False to catch the redirect to redirect_uri + response = knox_get(auth_url, params=params, auth=(self.username, self.password), verify=False, allow_redirects=False) + + # Should be a redirect to the callback URL + self.assertEqual(response.status_code, 303) + location = response.headers.get("Location") + self.assertIsNotNone(location) + self.assertTrue(location.startswith("http://localhost/callback")) + + parsed_url = urlparse(location) + query_params = parse_qs(parsed_url.query) + self.assertIn("code", query_params) + self.assertIn("state", query_params) + self.assertEqual(query_params["state"][0], "test_state") + code = query_params["code"][0] + + # 3. Exchange code for tokens + token_url = f"{self.knoxidf_token_url}knoxidf/api/v1/token" + print(f"Exchanging code for tokens at: {token_url}") + data = { + "grant_type": "authorization_code", + "code": code, + "redirect_uri": "http://localhost/callback", + "client_id": client_id, + "client_secret": client_secret + } + response = knox_post(token_url, data=data, verify=False) + if response.status_code != 200: + print(f"Code exchange error: {response.text}") + self.assertEqual(response.status_code, 200) + tokens = response.json() + self.assertIn("access_token", tokens) + self.assertIn("id_token", tokens) + self.assertIn("refresh_token", tokens) + + refresh_token = tokens["refresh_token"] + + print(f"Refresh token: {refresh_token}") + refresh_token_id = get_token_claim(refresh_token, 'knox.id') + print(f"Refresh token knox.id: {refresh_token_id}") + + # 4. Refresh the token (rotation) + print("Refreshing token...") + data = { + "grant_type": "refresh_token", + "refresh_token": refresh_token, + "client_id": client_id, + "client_secret": client_secret + } + response = knox_post(token_url, data=data, verify=False) + self.assertEqual(response.status_code, 200) + new_tokens = response.json() + self.assertIn("access_token", new_tokens) + self.assertIn("refresh_token", new_tokens) + + # Verify rotation: new refresh token should be different + self.assertNotEqual(refresh_token, new_tokens["refresh_token"]) + + # 5. Verify old refresh token is invalidated + print(f"Verifying old refresh token is invalidated...") + # Use same data (with old refresh_token) + data_old = { + "grant_type": "refresh_token", + "refresh_token": refresh_token, + "client_id": client_id, + "client_secret": client_secret + } + response = knox_post(token_url, data=data_old, verify=False, headers={"Accept": "application/json"}) + self.assertEqual(response.status_code, 401) + error_info = response.json() + display_id = get_token_id_display_text(refresh_token_id) + self.assertEqual(error_info["status"], "401") + self.assertIn(f"Unknown token: {display_id}", error_info["message"]) + +if __name__ == '__main__': + unittest.main() diff --git a/gateway-util-common/src/main/java/org/apache/knox/gateway/util/knoxidf/KnoxIDFUtils.java b/gateway-util-common/src/main/java/org/apache/knox/gateway/util/knoxidf/KnoxIDFUtils.java index ebcec5a016..432bf7a195 100644 --- a/gateway-util-common/src/main/java/org/apache/knox/gateway/util/knoxidf/KnoxIDFUtils.java +++ b/gateway-util-common/src/main/java/org/apache/knox/gateway/util/knoxidf/KnoxIDFUtils.java @@ -62,10 +62,10 @@ public static String joinFederatedToken(Map tokenMetadataMap, bo } public static Response error(String error, String description) { - final Map errorMeap = new HashMap<>(); - errorMeap.put("error", error); - errorMeap.put("error_description", description); - return Response.status(Response.Status.UNAUTHORIZED).entity(JsonUtils.renderAsJsonString(errorMeap)).build(); + final Map errorMap = new HashMap<>(); + errorMap.put("error", error); + errorMap.put("error_description", description); + return Response.status(Response.Status.UNAUTHORIZED).entity(JsonUtils.renderAsJsonString(errorMap)).build(); } public static String getRequestParamSafe(final HttpServletRequest request, final String key) { From 851aa8b09b7d48c5a890b84b4cc7f9f09d0597fe Mon Sep 17 00:00:00 2001 From: Sandor Molnar Date: Wed, 6 May 2026 15:25:54 +0200 Subject: [PATCH 13/14] KnoxIDF - Fix Docker-based test results publishing --- .github/workflows/publish-test-results.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/publish-test-results.yml b/.github/workflows/publish-test-results.yml index 0f584f599c..621830115d 100644 --- a/.github/workflows/publish-test-results.yml +++ b/.github/workflows/publish-test-results.yml @@ -46,5 +46,5 @@ jobs: commit: ${{ github.event.workflow_run.head_sha }} event_file: artifacts/Event File/event.json event_name: ${{ github.event.workflow_run.event }} - files: "artifacts/**/*.xml" + files: "artifacts/test-results/**/*.xml" From bd9245191d7919a4106c7d5b1600fa533ca5d633 Mon Sep 17 00:00:00 2001 From: Sandor Molnar Date: Wed, 6 May 2026 16:36:09 +0200 Subject: [PATCH 14/14] KnoxIDF - Fix Docker-based test results publishing; round 2 --- .github/workflows/tests.yml | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 7cd9e992d4..40f03845c4 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -97,19 +97,27 @@ jobs: name: test-results path: .github/workflows/tests/test-results.xml + - name: Archive Knox Logs + if: always() + run: tar -cvzf knox-logs.tar.gz -C .github/workflows/artifacts/knox-logs . + - name: Upload Knox Logs if: always() uses: actions/upload-artifact@v4 with: name: knox-logs - path: .github/workflows/artifacts/knox-logs + path: knox-logs.tar.gz + + - name: Archive Knox Conf + if: always() + run: tar -cvzf knox-conf.tar.gz -C .github/workflows/artifacts/knox-conf . - name: Upload Knox Conf if: always() uses: actions/upload-artifact@v4 with: name: knox-conf - path: .github/workflows/artifacts/knox-conf + path: knox-conf.tar.gz - name: Upload Event File uses: actions/upload-artifact@v4