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);
+
+ 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..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,23 +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.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;
@@ -44,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-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..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
@@ -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 = "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";
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/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/deploy/DeploymentFactory.java b/gateway-server/src/main/java/org/apache/knox/gateway/deploy/DeploymentFactory.java
index dfe4a4ea90..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
@@ -376,6 +376,13 @@ 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-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..f78f96af6d
--- /dev/null
+++ b/gateway-server/src/main/java/org/apache/knox/gateway/services/factory/FederatedIdentityServiceFactory.java
@@ -0,0 +1,97 @@
+/*
+ * 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.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;
+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 org.apache.knox.gateway.services.topology.TopologyService;
+import org.apache.knox.gateway.topology.Topology;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+
+public class FederatedIdentityServiceFactory extends AbstractServiceFactory {
+
+ private static final GatewayMessages LOG = MessagesFactory.get(GatewayMessages.class);
+ 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 {
+
+ 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(implementationToUse)) {
+ if (matchesImplementation(implementationToUse, EmptyFederatedIdentitityService.class, true)) {
+ service = new EmptyFederatedIdentitityService();
+ } 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(implementationToUse, e.getMessage(), e);
+ service = new EmptyFederatedIdentitityService();
+ }
+ } catch (Exception e) {
+ throw new ServiceLifecycleException("Error while creating Federated Identity Service: " + e, e);
+ }
+ }
+ 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;
+ }
+
+ @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/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
new file mode 100644
index 0000000000..8e26bdc1d5
--- /dev/null
+++ b/gateway-server/src/main/java/org/apache/knox/gateway/services/knoxidf/federation/JdbcFederatedIdentityService.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.services.knoxidf.federation;
+
+import org.apache.knox.gateway.config.GatewayConfig;
+import org.apache.knox.gateway.database.DataSourceProvider;
+import org.apache.knox.gateway.i18n.messages.MessagesFactory;
+import org.apache.knox.gateway.services.ServiceLifecycleException;
+import org.apache.knox.gateway.services.security.AliasService;
+
+import java.sql.SQLException;
+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;
+
+public class JdbcFederatedIdentityService implements FederatedIdentityService {
+ private static final FederatedIdentityServiceMessages LOG = MessagesFactory.get(FederatedIdentityServiceMessages.class);
+
+ private final AtomicBoolean initialized = new AtomicBoolean(false);
+ private final Lock initLock = new ReentrantLock(true);
+ 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 {
+ 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();
+ }
+ }
+ }
+
+ @Override
+ public void start() throws ServiceLifecycleException {
+ }
+
+ @Override
+ public void stop() throws ServiceLifecycleException {
+ }
+
+ public void setAliasService(AliasService aliasService) {
+ this.aliasService = aliasService;
+ }
+
+ protected AliasService getAliasService() {
+ return aliasService;
+ }
+
+ @Override
+ public void addFederatedIdentity(FederatedIdentity identity) {
+ try {
+ if (findByProviderAndSubject(identity.getProvider(), identity.getExternalIssuer(), identity.getExternalSubject()).isEmpty()) {
+ federatedIdentityDatabase.addFederatedIdentity(identity);
+ }
+ } catch (SQLException 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 {
+ return federatedIdentityDatabase.findByProviderAndSubject(provider, issuer, subject);
+ } catch (SQLException e) {
+ LOG.errorFetchingFederatedIdentityFromDatabase(provider, subject, issuer, e.getMessage(), e);
+ }
+ return Optional.empty();
+ }
+
+ @Override
+ public Optional findById(String id) {
+ try {
+ return federatedIdentityDatabase.findById(id);
+ } catch (SQLException 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/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..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,9 +16,14 @@
# 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
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 +33,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/createKnoxIDFFederatedIdentityAttributesTable.sql b/gateway-server/src/main/resources/createKnoxIDFFederatedIdentityAttributesTable.sql
new file mode 100644
index 0000000000..239cb5df1a
--- /dev/null
+++ b/gateway-server/src/main/resources/createKnoxIDFFederatedIdentityAttributesTable.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/createKnoxIDFFederatedIdentityAttributesTableDerby.sql b/gateway-server/src/main/resources/createKnoxIDFFederatedIdentityAttributesTableDerby.sql
new file mode 100644
index 0000000000..13ae9ab113
--- /dev/null
+++ b/gateway-server/src/main/resources/createKnoxIDFFederatedIdentityAttributesTableDerby.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/createKnoxIDFFederatedIdentityAttributesTableOracle.sql b/gateway-server/src/main/resources/createKnoxIDFFederatedIdentityAttributesTableOracle.sql
new file mode 100644
index 0000000000..7ed07b1b05
--- /dev/null
+++ b/gateway-server/src/main/resources/createKnoxIDFFederatedIdentityAttributesTableOracle.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/createKnoxIDFFederatedIdentityTable.sql b/gateway-server/src/main/resources/createKnoxIDFFederatedIdentityTable.sql
new file mode 100644
index 0000000000..acaf1c3404
--- /dev/null
+++ b/gateway-server/src/main/resources/createKnoxIDFFederatedIdentityTable.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/createKnoxIDFFederatedIdentityTableDerby.sql b/gateway-server/src/main/resources/createKnoxIDFFederatedIdentityTableDerby.sql
new file mode 100644
index 0000000000..7152d4c71d
--- /dev/null
+++ b/gateway-server/src/main/resources/createKnoxIDFFederatedIdentityTableDerby.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/createKnoxIDFFederatedIdentityTableOracle.sql b/gateway-server/src/main/resources/createKnoxIDFFederatedIdentityTableOracle.sql
new file mode 100644
index 0000000000..0dc42c1f72
--- /dev/null
+++ b/gateway-server/src/main/resources/createKnoxIDFFederatedIdentityTableOracle.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.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/AuthorizeResource.java b/gateway-service-knoxidf/src/main/java/org/apache/knox/gateway/service/knoxidf/AuthorizeResource.java
new file mode 100644
index 0000000000..94415de191
--- /dev/null
+++ b/gateway-service-knoxidf/src/main/java/org/apache/knox/gateway/service/knoxidf/AuthorizeResource.java
@@ -0,0 +1,408 @@
+/*
+ * 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.FederatedOpConfigurationStore;
+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.POST;
+import javax.ws.rs.Path;
+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.security.CommonTokenConstants.CLIENT_SECRET;
+import static org.apache.knox.gateway.security.CommonTokenConstants.GRANT_TYPE;
+import static org.apache.knox.gateway.util.knoxidf.KnoxIDFConstants.ALLOWED_SCOPES;
+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.CODE_CHALLENGE;
+import static org.apache.knox.gateway.util.knoxidf.KnoxIDFConstants.CODE_CHALLENGE_METHOD;
+import static org.apache.knox.gateway.util.knoxidf.KnoxIDFConstants.DEFAULT_SCOPES;
+import static org.apache.knox.gateway.util.knoxidf.KnoxIDFConstants.FEDERATED_IDENTITY_ID;
+import static org.apache.knox.gateway.util.knoxidf.KnoxIDFConstants.NONCE;
+import static org.apache.knox.gateway.util.knoxidf.KnoxIDFConstants.OFFLINE_ACCESS_SCOPE;
+import static org.apache.knox.gateway.util.knoxidf.KnoxIDFConstants.REDIRECT_URI;
+import static org.apache.knox.gateway.util.knoxidf.KnoxIDFConstants.REDIRECT_URIS;
+import static org.apache.knox.gateway.util.knoxidf.KnoxIDFConstants.RESPONSE_TYPE;
+import static org.apache.knox.gateway.util.knoxidf.KnoxIDFConstants.SCOPE;
+import static org.apache.knox.gateway.util.knoxidf.KnoxIDFConstants.STATE;
+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 AuthorizeRequestMetadataStore authorizeRequestMetadataStore;
+ private final FederatedOpConfigurationStore federatedOpConfigurationStore = FederatedOpConfigurationStore.getInstance(120000L);
+
+ @Context
+ private HttpServletRequest request;
+
+ @Context
+ private ServletContext servletContext;
+
+ private FederatedIdentityService federatedIdentityService;
+
+ @PostConstruct
+ @Override
+ public void init() throws ServletException, AliasServiceException, ServiceLifecycleException, KeyLengthException {
+ super.init();
+ this.authorizeRequestMetadataStore = AuthorizeRequestMetadataStore.getInstance(tokenTTL);
+ final GatewayServices services = (GatewayServices) servletContext.getAttribute(GatewayServices.GATEWAY_SERVICES_ATTRIBUTE);
+ federatedIdentityService = services.getService(ServiceType.KNOXIDF_FEDERATED_IDENTITY_SERVICE);
+ }
+
+ @Override
+ @GET
+ public Response doGet() {
+ return authorize();
+ }
+
+ @Override
+ @POST
+ public Response doPost() {
+ return authorize();
+ }
+
+ private Response authorize() {
+ return authorize(request.getParameter(RESPONSE_TYPE), request.getParameter(CLIENT_ID), request.getParameter(REDIRECT_URI),
+ request.getParameter(SCOPE), request.getParameter(STATE), request.getParameter(NONCE),
+ request.getParameter(CODE_CHALLENGE), request.getParameter(CODE_CHALLENGE_METHOD));
+ }
+
+ private Response authorize(String responseType,
+ String clientId,
+ String redirectUri,
+ String scope,
+ String state,
+ String nonce,
+ String codeChallenge,
+ String codeChallengeMethod) {
+ 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, codeChallenge, codeChallengeMethod);
+ 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.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);
+ 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) {
+ 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) {
+ final String redirectLocation;
+ try {
+ redirectLocation = authorizeRequestMetadata.getRedirectUri()
+ + "?code=" + URLEncoder.encode(code, UTF_8)
+ + "&state=" + URLEncoder.encode(authorizeRequestMetadata.getState(), UTF_8);
+ } catch (UnsupportedEncodingException e) {
+ throw new RuntimeException(e); //This should never happen with 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.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()));
+ }
+
+ @GET
+ @Path("/consentAccepted")
+ public Response consentAccepted() throws Exception {
+ final String state = request.getParameter(STATE);
+ final AuthorizeRequestMetadata authorizeRequestMetadata = authorizeRequestMetadataStore.get(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(),
+ authorizeRequestMetadata.getCodeChallenge(),
+ authorizeRequestMetadata.getCodeChallengeMethod());
+ }
+
+ @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) {
+ 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(TokenMetadata.USER_NAME, 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());
+ }
+ if (StringUtils.isNotBlank(authorizeRequestMetadata.getCodeChallenge())) {
+ authCodeTokenMap.put(CODE_CHALLENGE, authorizeRequestMetadata.getCodeChallenge());
+ authCodeTokenMap.put(CODE_CHALLENGE_METHOD, StringUtils.defaultIfBlank(authorizeRequestMetadata.getCodeChallengeMethod(), "plain"));
+ }
+ 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, FederatedOpConfiguration opConfig) {
+ String federatedIdToken = null;
+ String federatedAccessToken = null;
+ 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");
+ 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, FederatedOpConfiguration opConfig) {
+ final List params = new ArrayList<>();
+ params.add(new BasicNameValuePair(CODE, code));
+ 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()));
+
+ try (CloseableHttpClient httpClient = HttpClients.createDefault()) {
+ HttpPost post = new HttpPost(opConfig.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, String opName) throws ParseException {
+ final JWT jwt = new JWTToken(federatedIdToken);
+ final String issuer = jwt.getIssuer();
+ final String subject = jwt.getSubject();
+ return federatedIdentityService.findByProviderAndSubject(opName.toUpperCase(Locale.US), issuer, subject).orElseGet(() -> persistFederatedIdentity(jwt, opName));
+ }
+
+ 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)
+ .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)
+ opName.toUpperCase(Locale.US), // 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..ce895549db
--- /dev/null
+++ b/gateway-service-knoxidf/src/main/java/org/apache/knox/gateway/service/knoxidf/DiscoveryResource.java
@@ -0,0 +1,76 @@
+/*
+ * 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.services.GatewayServices;
+import org.apache.knox.gateway.util.JsonUtils;
+import org.apache.knox.gateway.util.knoxidf.KnoxIDFConstants;
+
+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;
+import static org.apache.knox.gateway.util.knoxidf.KnoxIDFConstants.TOKEN_EXCHANGE_TOPOLOGY_NAME;
+
+@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);
+ 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, KnoxIDFConstants.REFRESH_TOKEN});
+ config.put("scopes_supported", KnoxIDFConstants.DEFAULT_SCOPES);
+ config.put("id_token_signing_alg_values_supported", new String[]{"RS256"});
+ config.put("code_challenge_methods_supported", new String[]{KnoxIDFConstants.PKCE_METHOD_PLAIN, KnoxIDFConstants.PKCE_METHOD_S256});
+ 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/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..54e697eb98
--- /dev/null
+++ b/gateway-service-knoxidf/src/main/java/org/apache/knox/gateway/service/knoxidf/TokenResource.java
@@ -0,0 +1,452 @@
+/*
+ * 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.knoxidf.userparams.UserParamsProvider;
+import org.apache.knox.gateway.service.knoxidf.userparams.UserParamsProviderFactory;
+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.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;
+
+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.nio.charset.StandardCharsets;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.text.ParseException;
+import java.util.Base64;
+import java.util.HashMap;
+import java.util.Map;
+
+import static org.apache.knox.gateway.security.CommonTokenConstants.GRANT_TYPE;
+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.CODE_CHALLENGE;
+import static org.apache.knox.gateway.util.knoxidf.KnoxIDFConstants.CODE_CHALLENGE_METHOD;
+import static org.apache.knox.gateway.util.knoxidf.KnoxIDFConstants.CODE_VERIFIER;
+import static org.apache.knox.gateway.util.knoxidf.KnoxIDFConstants.FEDERATED_IDENTITY_ID;
+import static org.apache.knox.gateway.util.knoxidf.KnoxIDFConstants.OFFLINE_ACCESS_SCOPE;
+import static org.apache.knox.gateway.util.knoxidf.KnoxIDFConstants.PKCE_METHOD_PLAIN;
+import static org.apache.knox.gateway.util.knoxidf.KnoxIDFConstants.PKCE_METHOD_S256;
+import static org.apache.knox.gateway.util.knoxidf.KnoxIDFConstants.REDIRECT_URI;
+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)
+@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;
+ private long refreshTokenTTL;
+
+ @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 = UserParamsProviderFactory.getUserParamsProvider(servletContext);
+ 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 grantType = getRequestParam(GRANT_TYPE);
+ if (REFRESH_TOKEN.equals(grantType)) {
+ return handleRefreshToken();
+ } else if (AUTH_CODE.equals(grantType)) {
+ return handleAuthorizationCodeFlow();
+ }
+ return error("invalid_request", "invalid grant type: " + grantType);
+ }
+
+ @Override
+ protected UserContext buildUserContext(HttpServletRequest request) {
+ try {
+ 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);
+ 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 = 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 the 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);
+
+ 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 Response handleRefreshToken() {
+ try {
+ 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 (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 {
+ final String associatedClientId = authCodeTokenMetadata.getMetadata(CLIENT_ID);
+ final String clientId = getRequestParam(CLIENT_ID);
+ if (!associatedClientId.equals(clientId)) {
+ throw new AuthTokenValidationError("Invalid client_id: " + clientId);
+ }
+ }
+
+ // PKCE validation
+ final String codeChallenge = authCodeTokenMetadata.getMetadata(CODE_CHALLENGE);
+ if (StringUtils.isNotBlank(codeChallenge)) {
+ final String codeChallengeMethod = authCodeTokenMetadata.getMetadata(CODE_CHALLENGE_METHOD);
+ final String codeVerifier = getRequestParam(CODE_VERIFIER);
+ if (StringUtils.isBlank(codeVerifier)) {
+ throw new AuthTokenValidationError("Missing code_verifier");
+ }
+ if (!validatePKCE(codeVerifier, codeChallenge, codeChallengeMethod)) {
+ throw new AuthTokenValidationError("Invalid code_verifier");
+ }
+ }
+ } catch (UnknownTokenException e) {
+ throw new AuthTokenValidationError("Unknown auth_code");
+ }
+ }
+
+ private boolean validatePKCE(String codeVerifier, String codeChallenge, String method) {
+ if (PKCE_METHOD_PLAIN.equals(method)) {
+ return codeVerifier.equals(codeChallenge);
+ } else if (PKCE_METHOD_S256.equals(method)) {
+ try {
+ return generateS256Challenge(codeVerifier).equals(codeChallenge);
+ } catch (NoSuchAlgorithmException e) {
+ return false;
+ }
+ }
+ return false;
+ }
+
+ private String generateS256Challenge(String codeVerifier) throws NoSuchAlgorithmException {
+ MessageDigest md = MessageDigest.getInstance("SHA-256");
+ byte[] digest = md.digest(codeVerifier.getBytes(StandardCharsets.UTF_8));
+ return Base64.getUrlEncoder().withoutPadding().encodeToString(digest);
+ }
+
+ private String generateIdToken(JWT accessToken, TokenMetadata authCodeTokenMetadata) throws TokenServiceException {
+ final boolean hasFederatedIdToken = authCodeTokenMetadata != null && StringUtils.isNotBlank(authCodeTokenMetadata.getMetadata(FEDERATED_IDENTITY_ID));
+
+ if (hasFederatedIdToken) {
+ return generateFederatedIdToken(accessToken, authCodeTokenMetadata);
+ } else {
+ return generateLocalIdToken(accessToken, authCodeTokenMetadata);
+ }
+ }
+
+ 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);
+
+ return issueToken(builder).toString();
+ }
+
+ 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 (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 issueToken(idTokenAttributesBuilder).toString();
+ }
+
+ 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) {
+ 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);
+ }
+ }
+
+ private static class RefreshTokenValidationError extends Exception {
+ RefreshTokenValidationError(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..c837605aed
--- /dev/null
+++ b/gateway-service-knoxidf/src/main/java/org/apache/knox/gateway/service/knoxidf/UserInfoResource.java
@@ -0,0 +1,141 @@
+/*
+ * 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.service.knoxidf.userparams.UserParamsProvider;
+import org.apache.knox.gateway.service.knoxidf.userparams.UserParamsProviderFactory;
+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.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;
+
+
+@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 = UserParamsProviderFactory.getUserParamsProvider(servletContext);
+ 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(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(SCOPE_ATTRIBUTE) == null ? "" : request.getAttribute(SCOPE_ATTRIBUTE).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/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/java/org/apache/knox/gateway/service/knoxidf/userparams/EmptyUserParamsProvider.java b/gateway-service-knoxidf/src/main/java/org/apache/knox/gateway/service/knoxidf/userparams/EmptyUserParamsProvider.java
new file mode 100644
index 0000000000..f39d3eb772
--- /dev/null
+++ b/gateway-service-knoxidf/src/main/java/org/apache/knox/gateway/service/knoxidf/userparams/EmptyUserParamsProvider.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.service.knoxidf.userparams;
+
+import java.util.HashMap;
+import java.util.Map;
+
+public class EmptyUserParamsProvider implements UserParamsProvider {
+
+ @Override
+ public Map getParamsFor(String subjectName, String scope) {
+ return new HashMap<>();
+ }
+}
diff --git a/gateway-service-knoxidf/src/main/java/org/apache/knox/gateway/service/knoxidf/userparams/LdapUserParamsProvider.java b/gateway-service-knoxidf/src/main/java/org/apache/knox/gateway/service/knoxidf/userparams/LdapUserParamsProvider.java
new file mode 100644
index 0000000000..a17248451b
--- /dev/null
+++ b/gateway-service-knoxidf/src/main/java/org/apache/knox/gateway/service/knoxidf/userparams/LdapUserParamsProvider.java
@@ -0,0 +1,201 @@
+/*
+ * 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.userparams;
+
+import org.apache.knox.gateway.service.knoxidf.OIDCScope;
+import org.apache.knox.gateway.services.GatewayServices;
+import org.apache.knox.gateway.services.ServiceType;
+import org.apache.knox.gateway.services.security.AliasService;
+import org.apache.knox.gateway.services.security.AliasServiceException;
+
+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 javax.servlet.ServletContext;
+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 {
+ private static final String PREFIX = "user.params.provider.ldap.";
+ static final String LDAP_URL = PREFIX + "url";
+ private static final String LDAP_BASE_DN = PREFIX + "baseDn";
+ private static final String LDAP_USER_DN_TEMPLATE = PREFIX + "userDnTemplate";
+ private static final String LDAP_SYSTEM_USER = PREFIX + "systemUser";
+ private static final String LDAP_SYSTEM_PASSWORD_ALIAS = PREFIX + "systemPasswordAlias";
+
+ // === Defaults point to Knox's demo LDAP ===
+ private static final String DEFAULT_BASE_DN = "dc=hadoop,dc=apache,dc=org";
+ private static final String DEFAULT_USER_DN_TEMPLATE = "uid=%s,ou=people," + DEFAULT_BASE_DN;
+ private static final String DEFAULT_SYSTEM_USER = "uid=admin,ou=people," + DEFAULT_BASE_DN;
+ private static final String DEFAULT_SYSTEM_PASSWORD = "admin-password";
+
+ private static final String[] ATTRIBUTES = {"cn", "sn", "givenName", "mail"};
+
+ private final String ldapUrl;
+ private final String ldapBaseDn;
+ private final String ldapUserDnTemplate;
+ private final String ldapSystemUser;
+ private final String ldapSystemPassword;
+
+ LdapUserParamsProvider(ServletContext servletContext) {
+ this.ldapUrl = servletContext.getInitParameter(LDAP_URL);
+ this.ldapBaseDn = getInitParamOrDefault(servletContext, LDAP_BASE_DN, DEFAULT_BASE_DN);
+ this.ldapUserDnTemplate = getInitParamOrDefault(servletContext, LDAP_USER_DN_TEMPLATE, DEFAULT_USER_DN_TEMPLATE);
+ this.ldapSystemUser = getInitParamOrDefault(servletContext, LDAP_SYSTEM_USER, DEFAULT_SYSTEM_USER);
+ this.ldapSystemPassword = getSystemPassword(servletContext);
+ }
+
+ private String getInitParamOrDefault(ServletContext servletContext, String key, String defaultValue) {
+ final String value = servletContext.getInitParameter(key);
+ return value == null ? defaultValue : value;
+ }
+
+ private String getSystemPassword(ServletContext servletContext) {
+ final GatewayServices services = (GatewayServices) servletContext.getAttribute(GatewayServices.GATEWAY_SERVICES_ATTRIBUTE);
+ final AliasService aliasService = services.getService(ServiceType.ALIAS_SERVICE);
+ try {
+ final char[] systemPassword = aliasService.getPasswordFromAliasForGateway(LDAP_SYSTEM_PASSWORD_ALIAS);
+ return systemPassword == null ? DEFAULT_SYSTEM_PASSWORD : new String(systemPassword);
+ } catch (AliasServiceException e) {
+ return DEFAULT_SYSTEM_PASSWORD;
+ }
+ }
+
+ @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, ldapUserDnTemplate, 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," + ldapBaseDn;
+ 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, ldapSystemUser);
+ env.put(Context.SECURITY_CREDENTIALS, ldapSystemPassword);
+ 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/userparams/UserParamsProvider.java b/gateway-service-knoxidf/src/main/java/org/apache/knox/gateway/service/knoxidf/userparams/UserParamsProvider.java
new file mode 100644
index 0000000000..d052ef0403
--- /dev/null
+++ b/gateway-service-knoxidf/src/main/java/org/apache/knox/gateway/service/knoxidf/userparams/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.userparams;
+
+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/userparams/UserParamsProviderFactory.java b/gateway-service-knoxidf/src/main/java/org/apache/knox/gateway/service/knoxidf/userparams/UserParamsProviderFactory.java
new file mode 100644
index 0000000000..7cdf319844
--- /dev/null
+++ b/gateway-service-knoxidf/src/main/java/org/apache/knox/gateway/service/knoxidf/userparams/UserParamsProviderFactory.java
@@ -0,0 +1,26 @@
+/*
+ * 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.userparams;
+
+import javax.servlet.ServletContext;
+
+public class UserParamsProviderFactory {
+ public static UserParamsProvider getUserParamsProvider(ServletContext servletContext) {
+ final String ldapUrl = servletContext.getInitParameter(LdapUserParamsProvider.LDAP_URL);
+ return ldapUrl == null ? new EmptyUserParamsProvider() : new LdapUserParamsProvider(servletContext);
+ }
+}
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..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
@@ -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,39 @@
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.FederatedOpConfiguration;
+import org.apache.knox.gateway.util.knoxidf.FederatedOpConfigurationStore;
+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 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;
+import static org.apache.knox.gateway.services.GatewayServices.GATEWAY_CLUSTER_ATTRIBUTE;
@Path( WebSSOResource.RESOURCE_PATH )
public class WebSSOResource {
@@ -108,13 +112,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 FederatedOpConfigurationStore federatedOpConfigurationStore = FederatedOpConfigurationStore.getInstance(120000L);
private String sameSiteValue;
@@ -226,6 +231,25 @@ 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 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
@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 c0ec90b9dd..969eb4543f 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 String TOKEN_ENABLE_DELEGATED_AUTH = TOKEN_PARAM_PREFIX + "enable.delegated.auth";
private static final long TOKEN_TTL_DEFAULT = 30000L;
@@ -187,6 +188,7 @@ public class TokenResource {
private Optional maxTokenLifetime = Optional.empty();
private int tokenLimitPerUser;
+ private Map hardCodedClaimMappings;
private boolean includeGroupsInTokenAllowed;
private String tokenIssuer;
private boolean enableDelegatedAuth;
@@ -364,9 +366,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";
@@ -669,7 +699,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.";
@@ -721,22 +751,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})
@@ -844,16 +858,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 {
@@ -939,10 +954,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;
}
}
@@ -1014,13 +1035,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);
@@ -1034,7 +1052,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;
@@ -1078,7 +1096,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);
@@ -1086,8 +1104,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)
@@ -1118,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/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-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 1d19a846d7..7eb890e29d 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
@@ -21,6 +21,7 @@
import java.net.URISyntaxException;
import java.util.Date;
import java.util.List;
+import java.util.Map;
import java.util.Set;
public class JWTokenAttributes {
@@ -28,6 +29,7 @@ public class JWTokenAttributes {
public static final String DEFAULT_TYPE = "JWT";
private final String userName;
private final List audiences;
+ private final long issueTime;
private final String algorithm;
private final long expires;
private final String signingKeystoreName;
@@ -41,22 +43,14 @@ public class JWTokenAttributes {
private String kid;
private final String clientId;
private final String actor;
+ private final Map customAttributes;
- 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, audiences, algorithm, expires, signingKeystoreName, signingKeystoreAlias, signingKeystorePassphrase, managed, jku, type, groups, kid, issuer, clientId, 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, String actor) {
+ 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, String actor, Map customAttributes) {
this.userName = userName;
this.audiences = audiences;
this.algorithm = algorithm;
+ this.issueTime = issueTime;
this.expires = expires;
this.signingKeystoreName = signingKeystoreName;
this.signingKeystoreAlias = signingKeystoreAlias;
@@ -69,81 +63,89 @@ public class JWTokenAttributes {
this.issuer = issuer;
this.clientId = clientId;
this.actor = actor;
+ this.customAttributes = customAttributes;
}
+ public String getUserName() {
+ return userName;
+ }
- public String getUserName() {
- return userName;
- }
+ public List getAudiences() {
+ return audiences;
+ }
- public List getAudiences() {
- return audiences;
- }
+ public String getAlgorithm() {
+ return algorithm;
+ }
- public String getAlgorithm() {
- return algorithm;
- }
+ public long getIssueTime() {
+ return issueTime;
+ }
- public long getExpires() {
- return expires;
- }
+ public long getExpires() {
+ return expires;
+ }
- public Date getExpiresDate() {
- return expires == -1 ? null : new Date(expires);
- }
+ public Date getExpiresDate() {
+ return expires == -1 ? null : new Date(expires);
+ }
- public String getSigningKeystoreName() {
- return signingKeystoreName;
- }
+ public String getSigningKeystoreName() {
+ return signingKeystoreName;
+ }
- public String getSigningKeystoreAlias() {
- return signingKeystoreAlias;
- }
+ public String getSigningKeystoreAlias() {
+ return signingKeystoreAlias;
+ }
- public char[] getSigningKeystorePassphrase() {
- return signingKeystorePassphrase;
- }
+ public char[] getSigningKeystorePassphrase() {
+ return signingKeystorePassphrase;
+ }
- public boolean isManaged() {
- return managed;
- }
+ public boolean isManaged() {
+ return managed;
+ }
- public URI getJkuUri() throws URISyntaxException {
- return jku != null ? new URI(jku) : null;
- }
+ public URI getJkuUri() throws URISyntaxException {
+ return jku != null ? new URI(jku) : null;
+ }
- public String getJku(){
- return jku;
- }
+ public String getJku() {
+ return jku;
+ }
- public void setJku(String jku) {
- this.jku = jku;
- }
+ public void setJku(String jku) {
+ this.jku = jku;
+ }
- public String getType() {
- return type;
- }
+ public String getType() {
+ return type;
+ }
- public Set getGroups() {
- return groups;
- }
+ public Set getGroups() {
+ return groups;
+ }
- public void setKid(String kid) {
- this.kid = kid;
- }
+ public void setKid(String kid) {
+ this.kid = kid;
+ }
- public String getKid() {
- return kid;
- }
+ public String getKid() {
+ return kid;
+ }
- public String getIssuer() {
- return issuer;
- }
+ public String getIssuer() {
+ return issuer;
+ }
- public String getClientId() {
+ public String getClientId() {
return clientId;
}
- public String getActor() {
+ public String getActor() {
return actor;
}
+
+ 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 50896e61a9..6c2e4ab76c 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;
@@ -39,6 +41,7 @@ public class JWTokenAttributesBuilder {
private String issuer = JWTokenAttributes.DEFAULT_ISSUER;
private String clientId;
private String actor;
+ private Map customAttributes;
public JWTokenAttributesBuilder setUserName(String userName) {
this.userName = userName;
@@ -59,6 +62,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;
@@ -119,8 +127,13 @@ public JWTokenAttributesBuilder setActor(String actor) {
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, actor);
+ return new JWTokenAttributes(userName, (audiences == null ? new ArrayList<>() : audiences), algorithm, issueTime, expires, signingKeystoreName, signingKeystoreAlias,
+ signingKeystorePassphrase, managed, jku, type, groups, kid, issuer, clientId, actor, 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..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;
+ 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-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 4cb4d151ed..d756b034c0 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 {
@@ -63,6 +64,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 27f8f6ced9..2b779ab669 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;
@@ -82,6 +83,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());
@@ -114,6 +116,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);
@@ -148,6 +155,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..239e0baf2c
--- /dev/null
+++ b/gateway-util-common/src/main/java/org/apache/knox/gateway/util/knoxidf/AuthorizeRequestMetadata.java
@@ -0,0 +1,124 @@
+/*
+ * 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;
+ private final String codeChallenge;
+ private final String codeChallengeMethod;
+
+ public AuthorizeRequestMetadata(String clientId, String subject, String responseType, String redirectUri, Set requestedScopes, String state, String nonce) {
+ this(clientId, subject, responseType, redirectUri, requestedScopes, state, nonce, null, null);
+ }
+
+ public AuthorizeRequestMetadata(String clientId, String subject, String responseType, String redirectUri, Set requestedScopes, String state, String nonce, String codeChallenge, String codeChallengeMethod) {
+ this.clientId = clientId;
+ this.subject = subject;
+ this.responseType = responseType;
+ this.redirectUri = redirectUri;
+ this.requestedScopes = requestedScopes;
+ this.state = state;
+ this.nonce = nonce;
+ this.codeChallenge = codeChallenge;
+ this.codeChallengeMethod = codeChallengeMethod;
+ }
+
+ 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 String getCodeChallenge() {
+ return codeChallenge;
+ }
+
+ public String getCodeChallengeMethod() {
+ return codeChallengeMethod;
+ }
+
+ 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..80f3d7110f
--- /dev/null
+++ b/gateway-util-common/src/main/java/org/apache/knox/gateway/util/knoxidf/AuthorizeRequestMetadataStore.java
@@ -0,0 +1,33 @@
+/*
+ * 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;
+
+public class AuthorizeRequestMetadataStore extends KnoxIDFArtifactStore{
+
+ private static AuthorizeRequestMetadataStore instance;
+
+ private AuthorizeRequestMetadataStore(long ttl) {
+ super(ttl);
+ }
+
+ public static synchronized AuthorizeRequestMetadataStore getInstance(long ttl) {
+ if (instance == null) {
+ instance = new AuthorizeRequestMetadataStore(ttl);
+ }
+ return instance;
+ }
+}
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..7ad6078433
--- /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, final String opName) {
+ 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");
+ 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() {
+ return name;
+ }
+
+ public boolean isEnabled() {
+ 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..f1efc3d75b
--- /dev/null
+++ b/gateway-util-common/src/main/java/org/apache/knox/gateway/util/knoxidf/FederatedOpConfigurationFactory.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.util.knoxidf;
+
+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) {
+ 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(",")) {
+ 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/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
new file mode 100644
index 0000000000..5573b90e24
--- /dev/null
+++ b/gateway-util-common/src/main/java/org/apache/knox/gateway/util/knoxidf/KnoxIDFConstants.java
@@ -0,0 +1,60 @@
+/*
+ * 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 AUTH_CODE = "authorization_code";
+ String CLIENT_ID = "client_id";
+ String REDIRECT_URI = "redirect_uri";
+ String REDIRECT_URIS = "redirect_uris";
+ String RESPONSE_TYPE = "response_type";
+ Set ALLOWED_RESPONSE_TYPES = Sets.newHashSet("code", "id_token", "code id_token");
+ String SCOPE = "scope";
+ String ALLOWED_SCOPES = "allowed_scopes";
+ 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";
+
+ String CODE_CHALLENGE = "code_challenge";
+ String CODE_CHALLENGE_METHOD = "code_challenge_method";
+ String CODE_VERIFIER = "code_verifier";
+ String PKCE_METHOD_S256 = "S256";
+ String PKCE_METHOD_PLAIN = "plain";
+
+ String TOKEN_ID_ATTRIBUTE = "X-Token-Id";
+ String SCOPE_ATTRIBUTE = "X-Token-Scope";
+
+ String FEDERATED_IDENTITY_ID = "federated_identity_id";
+ 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_NAMES = FEDERATED_OP_CONFIG_PREFIX + "names";
+
+ String TOKEN_EXCHANGE_TOPOLOGY_NAME = "token.exchange.topology.name";
+}
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..989a17e612
--- /dev/null
+++ b/gateway-util-common/src/main/java/org/apache/knox/gateway/util/knoxidf/KnoxIDFUtils.java
@@ -0,0 +1,107 @@
+/*
+ * 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 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) {
+ 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();
+ return servletContext == null ? Collections.emptySet() : new HashSet<>(FederatedOpConfigurationFactory.createFederatedOpConfiguration(servletContext).values());
+ }
+
+ 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);
+ final String codeChallenge = request.getParameter(KnoxIDFConstants.CODE_CHALLENGE);
+ final String codeChallengeMethod = request.getParameter(KnoxIDFConstants.CODE_CHALLENGE_METHOD);
+ return new AuthorizeRequestMetadata(clientId, null, responseType, redirectUri, requestedScopes, state, nonce, codeChallenge, codeChallengeMethod);
+ }
+
+ 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 4e3cf3d351..64d09811a3 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
@@ -1407,6 +1409,11 @@
knox-token-generation-ui
${project.version}
+
+ org.apache.knox
+ gateway-service-knoxidf
+ ${project.version}
+
org.glassfish.jersey.containers
jersey-container-servlet-core
@@ -1422,6 +1429,11 @@
jersey-server
${jersey.version}
+
+ org.glassfish.jersey.core
+ jersey-common
+ ${jersey.version}
+
org.glassfish.jersey.inject
@@ -2017,6 +2029,11 @@
woodstox-core
${woodstox-core.version}
+
+ com.fasterxml.uuid
+ java-uuid-generator
+ ${uuid.generator.version}
+
cglib