This is an automated email from the ASF dual-hosted git repository.

smolnar82 pushed a commit to branch knox_idf
in repository https://gitbox.apache.org/repos/asf/knox.git


The following commit(s) were added to refs/heads/knox_idf by this push:
     new 33c51e855 KNOX-3355 - Add OIDCDiscoveryHelper, 
JdbcTrustedOidcIssuerService, and TrustedOidcIssuerServiceFactory (#1315)
33c51e855 is described below

commit 33c51e855b8d129723bc18ac6f7761460c38e0af
Author: hsheinblatt <[email protected]>
AuthorDate: Wed Jul 22 06:25:19 2026 -0700

    KNOX-3355 - Add OIDCDiscoveryHelper, JdbcTrustedOidcIssuerService, and 
TrustedOidcIssuerServiceFactory (#1315)
---
 gateway-server/pom.xml                             |   4 +
 .../apache/knox/gateway/database/DatabaseType.java |  29 +-
 .../gateway/services/DefaultGatewayServices.java   |   2 +
 .../factory/TrustedOidcIssuerServiceFactory.java   | 105 +++++++
 .../EmptyTrustedOidcIssuerService.java             |  81 +++++
 .../JdbcTrustedOidcIssuerService.java              | 200 +++++++++++++
 .../trustedoidcissuer/OIDCDiscoveryHelper.java     | 176 +++++++++++
 .../TrustedOidcIssuerDatabase.java                 | 101 +++++++
 .../TrustedOidcIssuerServiceMessages.java          |  51 ++++
 ...org.apache.knox.gateway.services.ServiceFactory |   1 +
 .../EmptyTrustedOidcIssuerServiceTest.java         |  64 ++++
 .../JdbcTrustedOidcIssuerServiceTest.java          | 325 +++++++++++++++++++++
 .../trustedoidcissuer/OIDCDiscoveryHelperTest.java | 303 +++++++++++++++++++
 .../TrustedOidcIssuerServiceFactoryTest.java       | 265 +++++++++++++++++
 .../TrustedOidcIssuersSchemaTest.java              |   2 +
 .../gateway/util/knoxidf/KnoxIDFConstants.java     |  13 +
 pom.xml                                            |   6 +
 17 files changed, 1720 insertions(+), 8 deletions(-)

diff --git a/gateway-server/pom.xml b/gateway-server/pom.xml
index 3ddfb07ab..012ffe8af 100644
--- a/gateway-server/pom.xml
+++ b/gateway-server/pom.xml
@@ -417,6 +417,10 @@
             <groupId>com.nimbusds</groupId>
             <artifactId>nimbus-jose-jwt</artifactId>
         </dependency>
+        <dependency>
+            <groupId>com.nimbusds</groupId>
+            <artifactId>oauth2-oidc-sdk</artifactId>
+        </dependency>
 
         <dependency>
             <groupId>org.apache.knox</groupId>
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 3f627bdac..5052d5d5a 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
@@ -24,7 +24,8 @@ public enum DatabaseType {
             
AbstractDataSourceFactory.KNOX_PROVIDERS_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
+            
AbstractDataSourceFactory.KNOXIDF_FED_IDENTITY_ATTR_TABLE_CREATE_SQL_FILE_NAME,
+            AbstractDataSourceFactory.KNOXIDF_TRUSTED_OIDC_ISSUERS_TABLE_SQL
     ),
     MYSQL("mysql",
             AbstractDataSourceFactory.TOKENS_TABLE_CREATE_SQL_FILE_NAME,
@@ -32,7 +33,8 @@ public enum DatabaseType {
             
AbstractDataSourceFactory.KNOX_PROVIDERS_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
+            
AbstractDataSourceFactory.KNOXIDF_FED_IDENTITY_ATTR_TABLE_CREATE_SQL_FILE_NAME,
+            AbstractDataSourceFactory.KNOXIDF_TRUSTED_OIDC_ISSUERS_TABLE_SQL
     ),
     MARIADB("mariadb",
             AbstractDataSourceFactory.TOKENS_TABLE_CREATE_SQL_FILE_NAME,
@@ -40,7 +42,8 @@ public enum DatabaseType {
             
AbstractDataSourceFactory.KNOX_PROVIDERS_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
+            
AbstractDataSourceFactory.KNOXIDF_FED_IDENTITY_ATTR_TABLE_CREATE_SQL_FILE_NAME,
+            AbstractDataSourceFactory.KNOXIDF_TRUSTED_OIDC_ISSUERS_TABLE_SQL
     ),
     HSQL("hsql",
             AbstractDataSourceFactory.TOKENS_TABLE_CREATE_SQL_FILE_NAME,
@@ -48,7 +51,8 @@ public enum DatabaseType {
             
AbstractDataSourceFactory.KNOX_PROVIDERS_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
+            
AbstractDataSourceFactory.KNOXIDF_FED_IDENTITY_ATTR_TABLE_CREATE_SQL_FILE_NAME,
+            AbstractDataSourceFactory.KNOXIDF_TRUSTED_OIDC_ISSUERS_TABLE_SQL
     ),
     DERBY("derbydb",
             AbstractDataSourceFactory.DERBY_TOKENS_TABLE_CREATE_SQL_FILE_NAME,
@@ -56,8 +60,8 @@ public enum DatabaseType {
             
AbstractDataSourceFactory.DERBY_KNOX_PROVIDERS_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
-
+            
AbstractDataSourceFactory.DERBY_KNOXIDF_FED_IDENTITY_ATTR_TABLE_CREATE_SQL_FILE_NAME,
+            
AbstractDataSourceFactory.DERBY_KNOXIDF_TRUSTED_OIDC_ISSUERS_TABLE_SQL
     ),
     ORACLE("oracle",
             AbstractDataSourceFactory.ORACLE_TOKENS_TABLE_CREATE_SQL_FILE_NAME,
@@ -65,7 +69,8 @@ public enum DatabaseType {
             
AbstractDataSourceFactory.ORACLE_KNOX_PROVIDERS_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
+            
AbstractDataSourceFactory.ORACLE_KNOXIDF_FED_IDENTITY_ATTR_TABLE_CREATE_SQL_FILE_NAME,
+            
AbstractDataSourceFactory.ORACLE_KNOXIDF_TRUSTED_OIDC_ISSUERS_TABLE_SQL
     );
 
     private final String type;
@@ -75,8 +80,11 @@ public enum DatabaseType {
     private final String descriptorsTableSql;
     private final String federatedIdentityTableSql;
     private final String federatedIdentityAttrTableSql;
+    private final String trustedOidcIssuersTableSql;
 
-    DatabaseType(String type, String tokensTableSql, String metadataTableSql, 
String providersTableSql, String descriptorsTableSql, String 
federatedIdentityTableSql, String federatedIdentityAttrTableSql) {
+    DatabaseType(String type, String tokensTableSql, String metadataTableSql, 
String providersTableSql,
+        String descriptorsTableSql, String federatedIdentityTableSql, String 
federatedIdentityAttrTableSql,
+        String trustedOidcIssuersTableSql) {
         this.type = type;
         this.tokensTableSql = tokensTableSql;
         this.metadataTableSql = metadataTableSql;
@@ -84,6 +92,7 @@ public enum DatabaseType {
         this.descriptorsTableSql = descriptorsTableSql;
         this.federatedIdentityTableSql = federatedIdentityTableSql;
         this.federatedIdentityAttrTableSql = federatedIdentityAttrTableSql;
+        this.trustedOidcIssuersTableSql = trustedOidcIssuersTableSql;
     }
 
     public String type() {
@@ -114,6 +123,10 @@ public enum DatabaseType {
         return federatedIdentityAttrTableSql;
     }
 
+    public String trustedOidcIssuersTableSql() {
+        return trustedOidcIssuersTableSql;
+    }
+
     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/services/DefaultGatewayServices.java
 
b/gateway-server/src/main/java/org/apache/knox/gateway/services/DefaultGatewayServices.java
index 2d7d12f13..39cf0aca0 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
@@ -88,6 +88,8 @@ public class DefaultGatewayServices extends 
AbstractGatewayServices {
     addService(ServiceType.LDAP_SERVICE, gatewayServiceFactory.create(this, 
ServiceType.LDAP_SERVICE, config, options));
 
     addService(ServiceType.KNOXIDF_FEDERATED_IDENTITY_SERVICE, 
gatewayServiceFactory.create(this, 
ServiceType.KNOXIDF_FEDERATED_IDENTITY_SERVICE, config, options));
+
+    addService(ServiceType.TRUSTED_OIDC_ISSUER_SERVICE, 
gatewayServiceFactory.create(this, ServiceType.TRUSTED_OIDC_ISSUER_SERVICE, 
config, options));
   }
 
   @Override
diff --git 
a/gateway-server/src/main/java/org/apache/knox/gateway/services/factory/TrustedOidcIssuerServiceFactory.java
 
b/gateway-server/src/main/java/org/apache/knox/gateway/services/factory/TrustedOidcIssuerServiceFactory.java
new file mode 100644
index 000000000..daa8d11a4
--- /dev/null
+++ 
b/gateway-server/src/main/java/org/apache/knox/gateway/services/factory/TrustedOidcIssuerServiceFactory.java
@@ -0,0 +1,105 @@
+/*
+ * 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
+ * <p>
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * <p>
+ * 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.trustedoidcissuer.EmptyTrustedOidcIssuerService;
+import 
org.apache.knox.gateway.services.knoxidf.trustedoidcissuer.JdbcTrustedOidcIssuerService;
+import 
org.apache.knox.gateway.services.knoxidf.trustedoidcissuer.TrustedOidcIssuerService;
+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 TrustedOidcIssuerServiceFactory extends AbstractServiceFactory {
+
+  private static final GatewayMessages LOG = 
MessagesFactory.get(GatewayMessages.class);
+  private static final String DEFAULT_IMPLEMENTATION = 
EmptyTrustedOidcIssuerService.class.getName();
+
+  @Override
+  protected Service createService(GatewayServices gatewayServices, ServiceType 
serviceType,
+      GatewayConfig gatewayConfig, Map<String, String> options, String 
implementation)
+      throws ServiceLifecycleException {
+
+    String implementationToUse = implementation;
+    if (isEmptyDefaultImplementation(implementationToUse)) {
+      if (isKnoxIdfEnabledInAnyTopology(gatewayServices)) {
+        implementationToUse = JdbcTrustedOidcIssuerService.class.getName();
+      }
+    }
+
+    TrustedOidcIssuerService service = null;
+    if (shouldCreateService(implementationToUse)) {
+      if (matchesImplementation(implementationToUse, 
EmptyTrustedOidcIssuerService.class, true)) {
+        service = new EmptyTrustedOidcIssuerService();
+      } else if (matchesImplementation(implementationToUse, 
JdbcTrustedOidcIssuerService.class)) {
+        try {
+          final JdbcTrustedOidcIssuerService jdbcService = new 
JdbcTrustedOidcIssuerService();
+          jdbcService.setAliasService(getAliasService(gatewayServices));
+          jdbcService.init(gatewayConfig, options);
+          service = jdbcService;
+        } catch (ServiceLifecycleException e) {
+          LOG.errorInitializingService(implementationToUse, e.getMessage(), e);
+          service = new EmptyTrustedOidcIssuerService();
+        } catch (Exception e) {
+          throw new ServiceLifecycleException(
+              "Error while creating TrustedOidcIssuerService: " + e, e);
+        }
+      }
+      if (service != null) {
+        logServiceUsage(service.getClass().getName(), serviceType);
+      }
+    }
+    return service;
+  }
+
+  /**
+   * Returns true if any deployed topology contains a service with role {@code 
KNOXIDF}
+   * or {@code KNOXIDF_ADMIN}. The trusted issuer registry is activated by 
either role
+   * because the admin API ({@code KNOXIDF_ADMIN}) also needs to persist 
registrations.
+   */
+  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(
+            s -> "KNOXIDF".equals(s.getRole()) || 
"KNOXIDF_ADMIN".equals(s.getRole()))) {
+          return true;
+        }
+      }
+    }
+    return false;
+  }
+
+  @Override
+  protected ServiceType getServiceType() {
+    return ServiceType.TRUSTED_OIDC_ISSUER_SERVICE;
+  }
+
+  @Override
+  protected Collection<String> getKnownImplementations() {
+    return List.of(DEFAULT_IMPLEMENTATION, 
JdbcTrustedOidcIssuerService.class.getName());
+  }
+}
diff --git 
a/gateway-server/src/main/java/org/apache/knox/gateway/services/knoxidf/trustedoidcissuer/EmptyTrustedOidcIssuerService.java
 
b/gateway-server/src/main/java/org/apache/knox/gateway/services/knoxidf/trustedoidcissuer/EmptyTrustedOidcIssuerService.java
new file mode 100644
index 000000000..506b31d65
--- /dev/null
+++ 
b/gateway-server/src/main/java/org/apache/knox/gateway/services/knoxidf/trustedoidcissuer/EmptyTrustedOidcIssuerService.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
+ * <p>
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * <p>
+ * 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.trustedoidcissuer;
+
+import org.apache.knox.gateway.config.GatewayConfig;
+import org.apache.knox.gateway.services.ServiceLifecycleException;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+
+/**
+ * No-op stub used when the KNOXIDF or KNOXIDF_ADMIN service role is not 
deployed.
+ * Read methods return safe empty results; mutating methods throw
+ * {@link UnsupportedOperationException}.
+ */
+public class EmptyTrustedOidcIssuerService implements TrustedOidcIssuerService 
{
+
+  @Override
+  public void init(GatewayConfig config, Map<String, String> options) throws 
ServiceLifecycleException {
+  }
+
+  @Override
+  public void start() throws ServiceLifecycleException {
+  }
+
+  @Override
+  public void stop() throws ServiceLifecycleException {
+  }
+
+  @Override
+  public boolean isTrusted(String issuerUrl) {
+    return false;
+  }
+
+  @Override
+  public boolean isDynamicJwks(String issuerUrl) {
+    return false;
+  }
+
+  @Override
+  public Optional<String> resolveJwksUri(String issuerUrl) {
+    return Optional.empty();
+  }
+
+  @Override
+  public void refreshJwksUri(String issuerUrl) {
+  }
+
+  @Override
+  public void register(TrustedOidcIssuer issuer) {
+    throw new UnsupportedOperationException("TrustedOidcIssuerService is not 
enabled; "
+        + "deploy the KNOXIDF or KNOXIDF_ADMIN service role to activate it.");
+  }
+
+  @Override
+  public void deregister(String issuerUrl) {
+    throw new UnsupportedOperationException("TrustedOidcIssuerService is not 
enabled; "
+        + "deploy the KNOXIDF or KNOXIDF_ADMIN service role to activate it.");
+  }
+
+  @Override
+  public List<TrustedOidcIssuer> list() {
+    return Collections.emptyList();
+  }
+}
diff --git 
a/gateway-server/src/main/java/org/apache/knox/gateway/services/knoxidf/trustedoidcissuer/JdbcTrustedOidcIssuerService.java
 
b/gateway-server/src/main/java/org/apache/knox/gateway/services/knoxidf/trustedoidcissuer/JdbcTrustedOidcIssuerService.java
new file mode 100644
index 000000000..0b1e0a31e
--- /dev/null
+++ 
b/gateway-server/src/main/java/org/apache/knox/gateway/services/knoxidf/trustedoidcissuer/JdbcTrustedOidcIssuerService.java
@@ -0,0 +1,200 @@
+/*
+ * 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
+ * <p>
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * <p>
+ * 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.trustedoidcissuer;
+
+import org.apache.hadoop.conf.Configuration;
+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 org.apache.knox.gateway.util.knoxidf.KnoxIDFConstants;
+
+import java.sql.SQLException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReentrantLock;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+/**
+ * JDBC-backed implementation of {@link TrustedOidcIssuerService}.
+ * <p>
+ * Maintains an in-memory registry snapshot as an {@link AtomicReference} to 
an immutable
+ * {@link Map}. Reads ({@link #isTrusted}, {@link #isDynamicJwks}, {@link 
#list}) are
+ * lock-free and always see a consistent snapshot. Writes ({@link #register},
+ * {@link #deregister}) are synchronized: the DB is committed first, then the 
snapshot is
+ * rebuilt from a fresh SELECT to guarantee the in-memory state cannot diverge 
from
+ * persistent storage.
+ * <p>
+ * HA note: each Knox node maintains its own snapshot. A registration on node 
A updates
+ * that node's snapshot immediately; other nodes' snapshots remain stale until 
restart.
+ */
+public class JdbcTrustedOidcIssuerService implements TrustedOidcIssuerService {
+
+  private static final TrustedOidcIssuerServiceMessages LOG =
+      MessagesFactory.get(TrustedOidcIssuerServiceMessages.class);
+
+  static final String MAX_TRUSTED_ISSUERS_CONFIG = 
"gateway.trustedoidcissuer.max.issuers";
+  private static final int DEFAULT_MAX_TRUSTED_ISSUERS = 10_000;
+
+  private final AtomicBoolean initialized = new AtomicBoolean(false);
+  private final Lock initLock = new ReentrantLock(true);
+
+  private final AtomicReference<Map<String, TrustedOidcIssuer>> 
registrySnapshot =
+      new AtomicReference<>(Collections.emptyMap());
+
+  private AliasService aliasService;
+  private TrustedOidcIssuerDatabase database;
+  private OIDCDiscoveryHelper discoveryHelper;
+  private int maxTrustedIssuers;
+
+  @Override
+  public void init(GatewayConfig config, Map<String, String> options) throws 
ServiceLifecycleException {
+    if (!initialized.get()) {
+      initLock.lock();
+      try {
+        if (aliasService == null) {
+          throw new ServiceLifecycleException("The required AliasService 
reference has not been set.");
+        }
+        try {
+          int maxIssuers = DEFAULT_MAX_TRUSTED_ISSUERS;
+          long cacheTtlSecs = 
KnoxIDFConstants.TRUSTED_OIDC_ISSUER_DEFAULT_DISCOVERY_CACHE_TTL_SECS;
+          int connectTimeoutMs = 
KnoxIDFConstants.TRUSTED_OIDC_ISSUER_DEFAULT_DISCOVERY_CONNECT_TIMEOUT_MS;
+          int readTimeoutMs = 
KnoxIDFConstants.TRUSTED_OIDC_ISSUER_DEFAULT_DISCOVERY_READ_TIMEOUT_MS;
+
+          if (config instanceof Configuration) {
+            final Configuration conf = (Configuration) config;
+            maxIssuers = conf.getInt(MAX_TRUSTED_ISSUERS_CONFIG, 
DEFAULT_MAX_TRUSTED_ISSUERS);
+            cacheTtlSecs = 
conf.getLong(KnoxIDFConstants.TRUSTED_OIDC_ISSUER_DISCOVERY_CACHE_TTL_SECS,
+                
KnoxIDFConstants.TRUSTED_OIDC_ISSUER_DEFAULT_DISCOVERY_CACHE_TTL_SECS);
+            connectTimeoutMs = 
conf.getInt(KnoxIDFConstants.TRUSTED_OIDC_ISSUER_DISCOVERY_CONNECT_TIMEOUT_MS,
+                
KnoxIDFConstants.TRUSTED_OIDC_ISSUER_DEFAULT_DISCOVERY_CONNECT_TIMEOUT_MS);
+            readTimeoutMs = 
conf.getInt(KnoxIDFConstants.TRUSTED_OIDC_ISSUER_DISCOVERY_READ_TIMEOUT_MS,
+                
KnoxIDFConstants.TRUSTED_OIDC_ISSUER_DEFAULT_DISCOVERY_READ_TIMEOUT_MS);
+          }
+
+          this.maxTrustedIssuers = maxIssuers;
+          this.database = new TrustedOidcIssuerDatabase(
+              DataSourceProvider.getDataSource(config, aliasService), 
config.getDatabaseType());
+          this.discoveryHelper = new OIDCDiscoveryHelper(this, cacheTtlSecs,
+              OIDCDiscoveryHelper.buildHttpClient(connectTimeoutMs, 
readTimeoutMs));
+          reloadRegistrySnapshot();
+          initialized.set(true);
+        } catch (ServiceLifecycleException e) {
+          throw e;
+        } catch (Exception e) {
+          throw new ServiceLifecycleException("Error initializing 
JdbcTrustedOidcIssuerService: " + 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 boolean isTrusted(String issuerUrl) {
+    return registrySnapshot.get().containsKey(issuerUrl);
+  }
+
+  @Override
+  public boolean isDynamicJwks(String issuerUrl) {
+    final TrustedOidcIssuer entry = registrySnapshot.get().get(issuerUrl);
+    return entry != null && entry.isDynamicJwks();
+  }
+
+  @Override
+  public Optional<String> resolveJwksUri(String issuerUrl) {
+    return discoveryHelper.discoverJwksUri(issuerUrl);
+  }
+
+  @Override
+  public synchronized void register(TrustedOidcIssuer issuer) {
+    if (registrySnapshot.get().size() >= maxTrustedIssuers) {
+      throw new IllegalStateException(
+          "Cannot register issuer: MAX_TRUSTED_ISSUERS (" + maxTrustedIssuers 
+ ") reached");
+    }
+    try {
+      database.insert(issuer);
+    } catch (SQLException e) {
+      LOG.errorRegisteringIssuer(issuer.getIssuerUrl(), e.getMessage(), e);
+      throw new RuntimeException("Error registering trusted OIDC issuer: " + 
issuer.getIssuerUrl(), e);
+    }
+    reloadRegistrySnapshot();
+  }
+
+  @Override
+  public synchronized void deregister(String issuerUrl) {
+    try {
+      database.delete(issuerUrl);
+    } catch (SQLException e) {
+      LOG.errorDeregisteringIssuer(issuerUrl, e.getMessage(), e);
+      throw new RuntimeException("Error deregistering trusted OIDC issuer: " + 
issuerUrl, e);
+    }
+    reloadRegistrySnapshot();
+    discoveryHelper.invalidate(issuerUrl);
+  }
+
+  @Override
+  public void refreshJwksUri(String issuerUrl) {
+    if (isDynamicJwks(issuerUrl)) {
+      discoveryHelper.invalidate(issuerUrl);
+    }
+  }
+
+  @Override
+  public List<TrustedOidcIssuer> list() {
+    return new ArrayList<>(registrySnapshot.get().values());
+  }
+
+  /**
+   * Rebuilds the registry snapshot from the current DB state.
+   * Called on init, after register, and after deregister.
+   * Synchronized on this to prevent concurrent rebuilds from interleaving 
with mutations.
+   */
+  private synchronized void reloadRegistrySnapshot() {
+    try {
+      final Map<String, TrustedOidcIssuer> fresh = 
database.selectAll().stream()
+          .collect(Collectors.toMap(TrustedOidcIssuer::getIssuerUrl, 
Function.identity()));
+      registrySnapshot.set(Collections.unmodifiableMap(fresh));
+    } catch (Exception e) {
+      LOG.errorReloadingRegistrySnapshot(e.getMessage(), e);
+    }
+  }
+}
diff --git 
a/gateway-server/src/main/java/org/apache/knox/gateway/services/knoxidf/trustedoidcissuer/OIDCDiscoveryHelper.java
 
b/gateway-server/src/main/java/org/apache/knox/gateway/services/knoxidf/trustedoidcissuer/OIDCDiscoveryHelper.java
new file mode 100644
index 000000000..2c830bebb
--- /dev/null
+++ 
b/gateway-server/src/main/java/org/apache/knox/gateway/services/knoxidf/trustedoidcissuer/OIDCDiscoveryHelper.java
@@ -0,0 +1,176 @@
+/*
+ * 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
+ * <p>
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * <p>
+ * 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.trustedoidcissuer;
+
+import com.github.benmanes.caffeine.cache.Cache;
+import com.github.benmanes.caffeine.cache.Caffeine;
+import com.nimbusds.openid.connect.sdk.op.OIDCProviderMetadata;
+import org.apache.http.client.config.RequestConfig;
+import org.apache.http.client.methods.CloseableHttpResponse;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.impl.client.CloseableHttpClient;
+import org.apache.http.impl.client.DefaultHttpRequestRetryHandler;
+import org.apache.http.impl.client.HttpClients;
+import org.apache.http.util.EntityUtils;
+import org.apache.knox.gateway.i18n.messages.MessagesFactory;
+
+import java.net.URI;
+import java.util.Optional;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Fetches and caches JWKS URIs resolved from OIDC provider discovery documents
+ * (/.well-known/openid-configuration). Backed by a Caffeine time-based cache.
+ * <p>
+ * SSRF gate: {@link #discoverJwksUri(String)} returns {@link 
Optional#empty()} immediately
+ * for any issuer not registered for dynamic JWKS. No HTTP call is ever made 
for
+ * untrusted or static-JWKS issuers.
+ * <p>
+ * The {@link CloseableHttpClient} is injected at construction time so that 
tests can
+ * supply a mock and verify the full fetch-and-parse code path without 
overriding methods.
+ * Production callers use {@link #buildHttpClient(int, int)} to obtain a 
properly
+ * configured long-lived client.
+ */
+class OIDCDiscoveryHelper {
+
+  private static final TrustedOidcIssuerServiceMessages LOG =
+      MessagesFactory.get(TrustedOidcIssuerServiceMessages.class);
+
+  private static final String USER_AGENT = "Apache-Knox-OIDCDiscovery/1.0";
+  private static final int HTTP_RETRY_COUNT = 2;
+  // Idle connections in the pool are closed after this duration so the next 
cache-miss
+  // fetch always goes through a fresh connection rather than a potentially 
stale one.
+  private static final long IDLE_EVICTION_SECONDS = 60L;
+
+  private final TrustedOidcIssuerService trustedIssuers;
+  // OIDC discovery document cache: issuerUrl → jwks_uri resolved from 
discovery endpoint.
+  // Entries expire after cacheTtlSeconds and are re-fetched lazily on the 
next access.
+  private final Cache<String, String> discoveryDocumentCache;
+  private final CloseableHttpClient httpClient;
+
+  /**
+   * Creates an {@code OIDCDiscoveryHelper} with the supplied HTTP client. Use
+   * {@link #buildHttpClient(int, int)} to obtain the production-configured 
client.
+   */
+  OIDCDiscoveryHelper(TrustedOidcIssuerService trustedIssuers, long 
cacheTtlSeconds,
+      CloseableHttpClient httpClient) {
+    this.trustedIssuers = trustedIssuers;
+    this.discoveryDocumentCache = Caffeine.newBuilder()
+        .expireAfterWrite(cacheTtlSeconds, TimeUnit.SECONDS)
+        .build();
+    this.httpClient = httpClient;
+  }
+
+  /**
+   * Builds a production-configured {@link CloseableHttpClient} for OIDC 
discovery fetches.
+   * <p>
+   * {@code requestSentRetryEnabled=true}: Discovery endpoints are GET-only 
(idempotent by RFC 7231
+   * §4.2.2), so retrying after the request was sent is safe and covers the 
most common
+   * failure mode — connection reset mid-response.
+   * <p>
+   * {@code evictIdleConnections} + {@code evictExpiredConnections}: the 
client is held for the
+   * gateway process lifetime. Without eviction, pooled connections become 
stale when the remote
+   * server or a network middlebox closes them silently, causing the next 
fetch to fail with a
+   * {@code NoHttpResponseException} before the retry handler can save it.
+   */
+  static CloseableHttpClient buildHttpClient(int connectTimeoutMs, int 
readTimeoutMs) {
+    final RequestConfig requestConfig = RequestConfig.custom()
+        .setConnectTimeout(connectTimeoutMs)
+        .setSocketTimeout(readTimeoutMs)
+        .build();
+    return HttpClients.custom()
+        .setDefaultRequestConfig(requestConfig)
+        .setRetryHandler(new DefaultHttpRequestRetryHandler(HTTP_RETRY_COUNT, 
true))
+        .evictIdleConnections(IDLE_EVICTION_SECONDS, TimeUnit.SECONDS)
+        .evictExpiredConnections()
+        .build();
+  }
+
+  /**
+   * Returns the JWKS URI for the given issuer URL, resolving it via OIDC 
discovery if
+   * not already cached. Returns {@link Optional#empty()} immediately without 
any HTTP
+   * call if the issuer is not registered for dynamic JWKS — this is the 
primary SSRF gate.
+   * <p>
+   * {@code Cache.get(key, mappingFunction)} is atomic per key: concurrent 
cache misses for the
+   * same issuer block on a single {@link #fetchJwksUri} call and share its 
result. If
+   * {@link #fetchJwksUri} returns null (on any error), Caffeine does not 
cache null, so the
+   * next call retries transparently.
+   */
+  Optional<String> discoverJwksUri(String issuerUrl) {
+    if (!trustedIssuers.isDynamicJwks(issuerUrl)) {
+      return Optional.empty();
+    }
+    return Optional.ofNullable(discoveryDocumentCache.get(issuerUrl, 
this::fetchJwksUri));
+  }
+
+  /**
+   * Evicts the cached JWKS URI for the given issuer so the next call to
+   * {@link #discoverJwksUri(String)} re-fetches from the discovery endpoint.
+   */
+  void invalidate(String issuerUrl) {
+    discoveryDocumentCache.invalidate(issuerUrl);
+  }
+
+  /**
+   * Fetches the JWKS URI by retrieving and parsing the OIDC discovery 
document for the
+   * given issuer. The discovery URL is constructed by stripping any trailing 
slash from
+   * the issuer URL and appending {@code /.well-known/openid-configuration}.
+   * Returns null on any error so Caffeine does not cache the failure and the 
next call retries.
+   */
+  String fetchJwksUri(String issuerUrl) {
+    final String discoveryUrl = issuerUrl.replaceAll("/$", "") + 
"/.well-known/openid-configuration";
+    final String body = httpGet(issuerUrl, discoveryUrl);
+    if (body == null) {
+      return null;
+    }
+    try {
+      final URI jwksUri = OIDCProviderMetadata.parse(body).getJWKSetURI();
+      if (jwksUri == null) {
+        // Defensive: OIDC spec requires jwks_uri; Nimbus 11.x throws 
ParseException if absent,
+        // but a non-compliant or future-lenient implementation could return 
null here.
+        LOG.errorParsingDiscoveryDocument(issuerUrl,
+            "discovery document contains no jwks_uri", null);
+        return null;
+      }
+      return jwksUri.toString();
+    } catch (Exception e) {
+      LOG.errorParsingDiscoveryDocument(issuerUrl, e.getMessage(), e);
+      return null;
+    }
+  }
+
+  /**
+   * Executes a GET request against the given URL and returns the response 
body as a string.
+   * Logs any failure and returns null so the caller knows not to cache the 
result.
+   */
+  private String httpGet(String issuerUrl, String url) {
+    final HttpGet request = new HttpGet(url);
+    request.setHeader("User-Agent", USER_AGENT);
+    try (CloseableHttpResponse response = httpClient.execute(request)) {
+      final int statusCode = response.getStatusLine().getStatusCode();
+      if (statusCode != 200) {
+        LOG.errorFetchingDiscoveryDocument(issuerUrl, url, "HTTP " + 
statusCode,
+            new java.io.IOException("Non-200 status: " + statusCode));
+        return null;
+      }
+      return EntityUtils.toString(response.getEntity());
+    } catch (Exception e) {
+      LOG.errorFetchingDiscoveryDocument(issuerUrl, url, e.getMessage(), e);
+      return null;
+    }
+  }
+}
diff --git 
a/gateway-server/src/main/java/org/apache/knox/gateway/services/knoxidf/trustedoidcissuer/TrustedOidcIssuerDatabase.java
 
b/gateway-server/src/main/java/org/apache/knox/gateway/services/knoxidf/trustedoidcissuer/TrustedOidcIssuerDatabase.java
new file mode 100644
index 000000000..92d0fd5c5
--- /dev/null
+++ 
b/gateway-server/src/main/java/org/apache/knox/gateway/services/knoxidf/trustedoidcissuer/TrustedOidcIssuerDatabase.java
@@ -0,0 +1,101 @@
+/*
+ * 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
+ * <p>
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * <p>
+ * 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.trustedoidcissuer;
+
+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.ArrayList;
+import java.util.List;
+
+/**
+ * JDBC helper for the {@code TRUSTED_OIDC_ISSUERS} table.
+ * All SQL uses {@link PreparedStatement} with {@code ?} parameters only.
+ * Uses {@link ResultSet#getBoolean(String)} for the {@code dynamic_jwks} 
column,
+ * which correctly maps both BOOLEAN (standard/Derby) and NUMBER(1) (Oracle) 
values.
+ */
+class TrustedOidcIssuerDatabase extends KnoxDatabase {
+
+  static final String TABLE_NAME = "TRUSTED_OIDC_ISSUERS";
+
+  private static final String INSERT_SQL =
+      "INSERT INTO " + TABLE_NAME + " (issuer_url, dynamic_jwks, cluster_name, 
registered_at, registered_by) VALUES (?, ?, ?, ?, ?)";
+  private static final String DELETE_SQL =
+      "DELETE FROM " + TABLE_NAME + " WHERE issuer_url = ?";
+  private static final String SELECT_ALL_SQL =
+      "SELECT issuer_url, dynamic_jwks, cluster_name, registered_at, 
registered_by FROM " + TABLE_NAME;
+  private static final String COUNT_SQL =
+      "SELECT COUNT(*) FROM " + TABLE_NAME;
+
+  TrustedOidcIssuerDatabase(DataSource dataSource, String dbType) throws 
Exception {
+    super(dataSource);
+    final DatabaseType databaseType = DatabaseType.fromString(dbType);
+    createTableIfNotExists(TABLE_NAME, 
databaseType.trustedOidcIssuersTableSql());
+  }
+
+  void insert(TrustedOidcIssuer issuer) throws SQLException {
+    try (Connection connection = dataSource.getConnection();
+         PreparedStatement ps = connection.prepareStatement(INSERT_SQL)) {
+      ps.setString(1, issuer.getIssuerUrl());
+      ps.setBoolean(2, issuer.isDynamicJwks());
+      ps.setString(3, issuer.getClusterName());
+      ps.setTimestamp(4, Timestamp.from(issuer.getRegisteredAt()));
+      ps.setString(5, issuer.getRegisteredBy());
+      ps.executeUpdate();
+    }
+  }
+
+  void delete(String issuerUrl) throws SQLException {
+    try (Connection connection = dataSource.getConnection();
+         PreparedStatement ps = connection.prepareStatement(DELETE_SQL)) {
+      ps.setString(1, issuerUrl);
+      ps.executeUpdate();
+    }
+  }
+
+  List<TrustedOidcIssuer> selectAll() throws SQLException {
+    final List<TrustedOidcIssuer> result = new ArrayList<>();
+    try (Connection connection = dataSource.getConnection();
+         PreparedStatement ps = connection.prepareStatement(SELECT_ALL_SQL);
+         ResultSet rs = ps.executeQuery()) {
+      while (rs.next()) {
+        result.add(new TrustedOidcIssuer(
+            rs.getString("issuer_url"),
+            rs.getBoolean("dynamic_jwks"),
+            rs.getString("cluster_name"),
+            rs.getTimestamp("registered_at").toInstant(),
+            rs.getString("registered_by")
+        ));
+      }
+    }
+    return result;
+  }
+
+  int count() throws SQLException {
+    try (Connection connection = dataSource.getConnection();
+         PreparedStatement ps = connection.prepareStatement(COUNT_SQL);
+         ResultSet rs = ps.executeQuery()) {
+      return rs.next() ? rs.getInt(1) : 0;
+    }
+  }
+}
diff --git 
a/gateway-server/src/main/java/org/apache/knox/gateway/services/knoxidf/trustedoidcissuer/TrustedOidcIssuerServiceMessages.java
 
b/gateway-server/src/main/java/org/apache/knox/gateway/services/knoxidf/trustedoidcissuer/TrustedOidcIssuerServiceMessages.java
new file mode 100644
index 000000000..1918bec47
--- /dev/null
+++ 
b/gateway-server/src/main/java/org/apache/knox/gateway/services/knoxidf/trustedoidcissuer/TrustedOidcIssuerServiceMessages.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
+ * <p>
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * <p>
+ * 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.trustedoidcissuer;
+
+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.trustedoidcissuer.service")
+interface TrustedOidcIssuerServiceMessages {
+
+  @Message(level = MessageLevel.ERROR,
+      text = "Failed to fetch OIDC discovery document for issuer {0} from {1}: 
{2}")
+  void errorFetchingDiscoveryDocument(String issuerUrl, String discoveryUrl, 
String cause,
+      @StackTrace(level = MessageLevel.DEBUG) Exception e);
+
+  @Message(level = MessageLevel.ERROR,
+      text = "Failed to parse OIDC discovery document for issuer {0}: {1}")
+  void errorParsingDiscoveryDocument(String issuerUrl, String cause,
+      @StackTrace(level = MessageLevel.DEBUG) Exception e);
+
+  @Message(level = MessageLevel.ERROR,
+      text = "Error registering trusted OIDC issuer {0}: {1}")
+  void errorRegisteringIssuer(String issuerUrl, String cause,
+      @StackTrace(level = MessageLevel.DEBUG) Exception e);
+
+  @Message(level = MessageLevel.ERROR,
+      text = "Error deregistering trusted OIDC issuer {0}: {1}")
+  void errorDeregisteringIssuer(String issuerUrl, String cause,
+      @StackTrace(level = MessageLevel.DEBUG) Exception e);
+
+  @Message(level = MessageLevel.ERROR,
+      text = "Error reloading trusted OIDC issuer registry snapshot: {0}")
+  void errorReloadingRegistrySnapshot(String cause,
+      @StackTrace(level = MessageLevel.DEBUG) Exception e);
+}
diff --git 
a/gateway-server/src/main/resources/META-INF/services/org.apache.knox.gateway.services.ServiceFactory
 
b/gateway-server/src/main/resources/META-INF/services/org.apache.knox.gateway.services.ServiceFactory
index 93bc7845a..e67206f2c 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
@@ -38,3 +38,4 @@ 
org.apache.knox.gateway.services.factory.TopologyServiceFactory
 org.apache.knox.gateway.services.factory.LdapServiceFactory
 org.apache.knox.gateway.services.factory.LDAPRolesLookupServiceFactory
 org.apache.knox.gateway.services.factory.TokenServiceFactory
+org.apache.knox.gateway.services.factory.TrustedOidcIssuerServiceFactory
diff --git 
a/gateway-server/src/test/java/org/apache/knox/gateway/services/knoxidf/trustedoidcissuer/EmptyTrustedOidcIssuerServiceTest.java
 
b/gateway-server/src/test/java/org/apache/knox/gateway/services/knoxidf/trustedoidcissuer/EmptyTrustedOidcIssuerServiceTest.java
new file mode 100644
index 000000000..c606010a5
--- /dev/null
+++ 
b/gateway-server/src/test/java/org/apache/knox/gateway/services/knoxidf/trustedoidcissuer/EmptyTrustedOidcIssuerServiceTest.java
@@ -0,0 +1,64 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. The ASF
+ * licenses this file to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * <p>
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * <p>
+ * 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.trustedoidcissuer;
+
+import org.junit.Test;
+
+import java.time.Instant;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+public class EmptyTrustedOidcIssuerServiceTest {
+
+  private final EmptyTrustedOidcIssuerService service = new 
EmptyTrustedOidcIssuerService();
+
+  @Test
+  public void testIsTrustedReturnsFalse() {
+    assertFalse(service.isTrusted("https://any.issuer.com";));
+  }
+
+  @Test
+  public void testIsDynamicJwksReturnsFalse() {
+    assertFalse(service.isDynamicJwks("https://any.issuer.com";));
+  }
+
+  @Test
+  public void testResolveJwksUriReturnsEmpty() {
+    assertFalse(service.resolveJwksUri("https://any.issuer.com";).isPresent());
+  }
+
+  @Test
+  public void testRefreshJwksUriIsNoOp() {
+    service.refreshJwksUri("https://any.issuer.com";); // must not throw
+  }
+
+  @Test
+  public void testListReturnsEmpty() {
+    assertTrue(service.list().isEmpty());
+  }
+
+  @Test(expected = UnsupportedOperationException.class)
+  public void testRegisterThrows() {
+    service.register(new TrustedOidcIssuer("https://issuer.com";, false, null, 
Instant.now(), null));
+  }
+
+  @Test(expected = UnsupportedOperationException.class)
+  public void testDeregisterThrows() {
+    service.deregister("https://issuer.com";);
+  }
+}
diff --git 
a/gateway-server/src/test/java/org/apache/knox/gateway/services/knoxidf/trustedoidcissuer/JdbcTrustedOidcIssuerServiceTest.java
 
b/gateway-server/src/test/java/org/apache/knox/gateway/services/knoxidf/trustedoidcissuer/JdbcTrustedOidcIssuerServiceTest.java
new file mode 100644
index 000000000..03bc54f76
--- /dev/null
+++ 
b/gateway-server/src/test/java/org/apache/knox/gateway/services/knoxidf/trustedoidcissuer/JdbcTrustedOidcIssuerServiceTest.java
@@ -0,0 +1,325 @@
+/*
+ * 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
+ * <p>
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * <p>
+ * 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.trustedoidcissuer;
+
+import org.apache.knox.gateway.config.GatewayConfig;
+import org.apache.knox.gateway.config.impl.GatewayConfigImpl;
+import org.apache.knox.gateway.database.AbstractDataSourceFactory;
+import org.apache.knox.gateway.database.DatabaseType;
+import org.apache.knox.gateway.services.ServiceLifecycleException;
+import org.apache.knox.gateway.services.security.AliasService;
+import org.easymock.EasyMock;
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+import java.sql.Connection;
+import java.sql.DriverManager;
+import java.sql.PreparedStatement;
+import java.sql.SQLException;
+import java.time.Instant;
+import java.util.List;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+public class JdbcTrustedOidcIssuerServiceTest {
+
+  private static final String DB_NAME = "trustedissuers_svc_test";
+  private static final String DERBY_CREATE_URL = "jdbc:derby:memory:" + 
DB_NAME + ";create=true";
+  private static final String DERBY_URL = "jdbc:derby:memory:" + DB_NAME;
+  private static final String DERBY_SHUTDOWN_URL = "jdbc:derby:memory:" + 
DB_NAME + ";shutdown=true";
+
+  private static GatewayConfig gatewayConfig;
+  private static AliasService aliasService;
+
+  private JdbcTrustedOidcIssuerService service;
+
+  @BeforeClass
+  public static void setUpClass() throws Exception {
+    // Derby 10.14 does not recognize locales like en_001; force a standard 
locale.
+    java.util.Locale.setDefault(java.util.Locale.US);
+    // Create the Derby in-memory DB so DerbyDataSourceFactory can connect to 
it
+    DriverManager.getConnection(DERBY_CREATE_URL).close();
+
+    gatewayConfig = EasyMock.createNiceMock(GatewayConfig.class);
+    
EasyMock.expect(gatewayConfig.getDatabaseType()).andReturn(DatabaseType.DERBY.type()).anyTimes();
+    EasyMock.expect(gatewayConfig.getDatabaseName()).andReturn("memory:" + 
DB_NAME).anyTimes();
+    EasyMock.replay(gatewayConfig);
+
+    aliasService = EasyMock.createNiceMock(AliasService.class);
+    EasyMock.expect(aliasService.getPasswordFromAliasForGateway(
+        
AbstractDataSourceFactory.DATABASE_USER_ALIAS_NAME)).andReturn(null).anyTimes();
+    EasyMock.expect(aliasService.getPasswordFromAliasForGateway(
+        
AbstractDataSourceFactory.DATABASE_PASSWORD_ALIAS_NAME)).andReturn(null).anyTimes();
+    EasyMock.replay(aliasService);
+  }
+
+  @AfterClass
+  public static void tearDownClass() {
+    try {
+      DriverManager.getConnection(DERBY_SHUTDOWN_URL);
+    } catch (SQLException e) {
+      // Derby signals a successful in-memory shutdown as SQLState 08006 / 
error 45000
+      if (!(e.getErrorCode() == 45000 && "08006".equals(e.getSQLState()))) {
+        throw new RuntimeException("Unexpected Derby shutdown error", e);
+      }
+    }
+  }
+
+  @Before
+  public void setUp() throws ServiceLifecycleException, SQLException {
+    // Clear table between tests
+    try (Connection conn = DriverManager.getConnection(DERBY_URL);
+         PreparedStatement ps = conn.prepareStatement("DELETE FROM 
TRUSTED_OIDC_ISSUERS")) {
+      ps.executeUpdate();
+    } catch (SQLException e) {
+      // Table may not exist yet on first setUp; service.init() will create it
+    }
+
+    service = new JdbcTrustedOidcIssuerService();
+    service.setAliasService(aliasService);
+    service.init(gatewayConfig, null);
+  }
+
+  // ------------------------------------------------------------------
+  // Basic CRUD and snapshot
+  // ------------------------------------------------------------------
+
+  @Test
+  public void testRegisterAndIsTrusted() {
+    service.register(issuer("https://issuer.example.com";, false));
+
+    assertTrue(service.isTrusted("https://issuer.example.com";));
+    assertFalse(service.isTrusted("https://other.example.com";));
+  }
+
+  @Test
+  public void testDeregisterClearsSnapshot() {
+    service.register(issuer("https://issuer.example.com";, false));
+    assertTrue(service.isTrusted("https://issuer.example.com";));
+
+    service.deregister("https://issuer.example.com";);
+    assertFalse(service.isTrusted("https://issuer.example.com";));
+  }
+
+  @Test
+  public void testListReflectsSnapshot() {
+    final TrustedOidcIssuer a = issuer("https://a.example.com";, false, 
"clusterA", "admin");
+    final TrustedOidcIssuer b = issuer("https://b.example.com";, true, 
"clusterB", "operator");
+    service.register(a);
+    service.register(b);
+
+    final List<TrustedOidcIssuer> listed = service.list();
+    assertEquals(2, listed.size());
+    assertIssuerInList(a, listed);
+    assertIssuerInList(b, listed);
+  }
+
+  @Test
+  public void testDynamicJwksFlag() {
+    service.register(issuer("https://static.example.com";, false));
+    service.register(issuer("https://dynamic.example.com";, true));
+
+    assertFalse(service.isDynamicJwks("https://static.example.com";));
+    assertTrue(service.isDynamicJwks("https://dynamic.example.com";));
+    assertFalse("Unregistered issuer must return false",
+        service.isDynamicJwks("https://unknown.example.com";));
+  }
+
+  /**
+   * All fields must round-trip through the DB correctly, including nullable 
ones.
+   */
+  @Test
+  public void testRegisterPersistsAllFields() {
+    final TrustedOidcIssuer issuer = issuer("https://issuer.example.com";, 
true, "prod-cluster", "admin");
+    service.register(issuer);
+
+    final List<TrustedOidcIssuer> listed = service.list();
+    assertEquals(1, listed.size());
+    assertIssuerEquals(issuer, listed.get(0));
+  }
+
+  @Test
+  public void testRegisterPersistsNullableFieldsAsNull() {
+    // clusterName and registeredBy may be null
+    final TrustedOidcIssuer issuer = new TrustedOidcIssuer(
+        "https://issuer.example.com";, false, null, Instant.now(), null);
+    service.register(issuer);
+
+    final TrustedOidcIssuer fromList = service.list().get(0);
+    assertEquals("https://issuer.example.com";, fromList.getIssuerUrl());
+    assertFalse(fromList.isDynamicJwks());
+    assertNotNull("registeredAt must always be persisted", 
fromList.getRegisteredAt());
+    assertTrue("clusterName round-trips as null", fromList.getClusterName() == 
null
+        || fromList.getClusterName().isEmpty());
+    assertTrue("registeredBy round-trips as null", fromList.getRegisteredBy() 
== null
+        || fromList.getRegisteredBy().isEmpty());
+  }
+
+  @Test
+  public void testRegistrySnapshotWarmOnInit() throws Exception {
+    // Pre-populate the TRUSTED_OIDC_ISSUERS table before initializing a new 
service
+    final String preloadedUrl = "https://preloaded.example.com";;
+    try (Connection conn = DriverManager.getConnection(DERBY_URL);
+         PreparedStatement ps = conn.prepareStatement(
+             "INSERT INTO TRUSTED_OIDC_ISSUERS (issuer_url, dynamic_jwks, 
registered_at) "
+                 + "VALUES (?, ?, ?)")) {
+      ps.setString(1, preloadedUrl);
+      ps.setBoolean(2, false);
+      ps.setTimestamp(3, java.sql.Timestamp.from(Instant.now()));
+      ps.executeUpdate();
+    }
+
+    // New service instance: snapshot must be loaded from DB on startup
+    final JdbcTrustedOidcIssuerService freshService = new 
JdbcTrustedOidcIssuerService();
+    freshService.setAliasService(aliasService);
+    freshService.init(gatewayConfig, null);
+
+    assertTrue("Pre-populated issuer must be trusted after init", 
freshService.isTrusted(preloadedUrl));
+  }
+
+  @Test(expected = RuntimeException.class)
+  public void testDuplicateRegistrationThrows() {
+    final TrustedOidcIssuer issuer = issuer("https://issuer.example.com";, 
false);
+    service.register(issuer);
+    service.register(issuer); // duplicate primary key → RuntimeException
+  }
+
+  @Test
+  public void testReloadAfterMutation() {
+    final String url = "https://issuer.example.com";;
+
+    service.register(issuer(url, false));
+    assertTrue("Snapshot must contain issuer after register", 
service.isTrusted(url));
+    assertEquals(1, service.list().size());
+
+    service.deregister(url);
+    assertFalse("Snapshot must not contain issuer after deregister", 
service.isTrusted(url));
+    assertTrue(service.list().isEmpty());
+  }
+
+  @Test
+  public void testMaxTrustedIssuers() throws ServiceLifecycleException {
+    final GatewayConfigImpl limitedConfig = new GatewayConfigImpl();
+    limitedConfig.set(JdbcTrustedOidcIssuerService.MAX_TRUSTED_ISSUERS_CONFIG, 
"2");
+    limitedConfig.set(GatewayConfigImpl.GATEWAY_DATABASE_TYPE, 
DatabaseType.DERBY.type());
+    limitedConfig.set(GatewayConfigImpl.GATEWAY_DATABASE_NAME, "memory:" + 
DB_NAME);
+
+    final JdbcTrustedOidcIssuerService limitedService = new 
JdbcTrustedOidcIssuerService();
+    limitedService.setAliasService(aliasService);
+    limitedService.init(limitedConfig, null);
+
+    limitedService.register(issuer("https://a.example.com";, false));
+    assertEquals("First registration must succeed", 1, 
limitedService.list().size());
+
+    limitedService.register(issuer("https://b.example.com";, false));
+    assertEquals("Second registration must succeed", 2, 
limitedService.list().size());
+
+    try {
+      limitedService.register(issuer("https://c.example.com";, false));
+      fail("Expected IllegalStateException when exceeding max issuers limit");
+    } catch (IllegalStateException e) {
+      assertEquals("Prior registrations must be unaffected by the rejected 
call",
+          2, limitedService.list().size());
+    }
+  }
+
+  @Test
+  public void testDeregisterNonExistentIsNoOp() {
+    // deregister of unknown issuer must not throw
+    service.deregister("https://nonexistent.example.com";);
+    assertTrue(service.list().isEmpty());
+  }
+
+  // ------------------------------------------------------------------
+  // resolveJwksUri / refreshJwksUri delegation
+  // ------------------------------------------------------------------
+
+  @Test
+  public void testResolveJwksUriForNonDynamicIssuerReturnsEmpty() {
+    service.register(issuer("https://static.example.com";, false));
+    // Non-dynamic issuer: OIDCDiscoveryHelper.discoverJwksUri returns empty 
immediately
+    // without any HTTP call (the SSRF gate inside the helper blocks it).
+    
assertFalse(service.resolveJwksUri("https://static.example.com";).isPresent());
+  }
+
+  @Test
+  public void testResolveJwksUriForUnregisteredIssuerReturnsEmpty() {
+    
assertFalse(service.resolveJwksUri("https://unknown.example.com";).isPresent());
+  }
+
+  @Test
+  public void testRefreshJwksUriForNonDynamicIsNoOp() {
+    service.register(issuer("https://static.example.com";, false));
+    // refreshJwksUri checks isDynamicJwks first; for non-dynamic it is a no-op
+    service.refreshJwksUri("https://static.example.com";); // must not throw
+  }
+
+  @Test
+  public void testRefreshJwksUriForUnregisteredIsNoOp() {
+    service.refreshJwksUri("https://unknown.example.com";); // must not throw
+  }
+
+  // ------------------------------------------------------------------
+  // Init guard
+  // ------------------------------------------------------------------
+
+  @Test(expected = ServiceLifecycleException.class)
+  public void testInitFailsWithoutAliasService() throws 
ServiceLifecycleException {
+    final JdbcTrustedOidcIssuerService noAliasService = new 
JdbcTrustedOidcIssuerService();
+    // setAliasService NOT called
+    noAliasService.init(gatewayConfig, null);
+  }
+
+  // ------------------------------------------------------------------
+  // Helpers
+  // ------------------------------------------------------------------
+
+  private static TrustedOidcIssuer issuer(String url, boolean dynamicJwks) {
+    return new TrustedOidcIssuer(url, dynamicJwks, null, Instant.now(), null);
+  }
+
+  private static TrustedOidcIssuer issuer(String url, boolean dynamicJwks,
+      String clusterName, String registeredBy) {
+    return new TrustedOidcIssuer(url, dynamicJwks, clusterName, Instant.now(), 
registeredBy);
+  }
+
+  /**
+   * Asserts that all non-generated fields of {@code expected} match {@code 
actual}, and
+   * that the generated {@code registeredAt} field is non-null.
+   */
+  private static void assertIssuerEquals(TrustedOidcIssuer expected, 
TrustedOidcIssuer actual) {
+    assertEquals("issuerUrl", expected.getIssuerUrl(), actual.getIssuerUrl());
+    assertEquals("dynamicJwks", expected.isDynamicJwks(), 
actual.isDynamicJwks());
+    assertEquals("clusterName", expected.getClusterName(), 
actual.getClusterName());
+    assertEquals("registeredBy", expected.getRegisteredBy(), 
actual.getRegisteredBy());
+    assertNotNull("registeredAt must be persisted", actual.getRegisteredAt());
+  }
+
+  private static void assertIssuerInList(TrustedOidcIssuer expected, 
List<TrustedOidcIssuer> list) {
+    final TrustedOidcIssuer found = list.stream()
+        .filter(i -> expected.getIssuerUrl().equals(i.getIssuerUrl()))
+        .findFirst()
+        .orElseThrow(() -> new AssertionError("Issuer not found in list: " + 
expected.getIssuerUrl()));
+    assertIssuerEquals(expected, found);
+  }
+}
diff --git 
a/gateway-server/src/test/java/org/apache/knox/gateway/services/knoxidf/trustedoidcissuer/OIDCDiscoveryHelperTest.java
 
b/gateway-server/src/test/java/org/apache/knox/gateway/services/knoxidf/trustedoidcissuer/OIDCDiscoveryHelperTest.java
new file mode 100644
index 000000000..f978a38af
--- /dev/null
+++ 
b/gateway-server/src/test/java/org/apache/knox/gateway/services/knoxidf/trustedoidcissuer/OIDCDiscoveryHelperTest.java
@@ -0,0 +1,303 @@
+/*
+ * 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
+ * <p>
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * <p>
+ * 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.trustedoidcissuer;
+
+import org.apache.http.StatusLine;
+import org.apache.http.client.methods.CloseableHttpResponse;
+import org.apache.http.client.methods.HttpUriRequest;
+import org.apache.http.entity.StringEntity;
+import org.apache.http.impl.client.CloseableHttpClient;
+import org.easymock.Capture;
+import org.easymock.EasyMock;
+import org.junit.Test;
+
+import java.io.IOException;
+import java.util.Optional;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+public class OIDCDiscoveryHelperTest {
+
+  private static final String ISSUER = "https://issuer.example.com";;
+  private static final String ISSUER_WITH_SLASH = 
"https://issuer.example.com/";;
+  private static final String JWKS_URI = "https://issuer.example.com/jwks";;
+  private static final long CACHE_TTL = 600L;
+
+  // Minimal valid OIDC discovery document (all required fields per OpenID 
Connect Discovery 1.0)
+  private static final String VALID_DISCOVERY_JSON = "{"
+      + "\"issuer\":\"" + ISSUER + "\","
+      + "\"authorization_endpoint\":\"https://issuer.example.com/authorize\",";
+      + "\"jwks_uri\":\"" + JWKS_URI + "\","
+      + "\"response_types_supported\":[\"code\"],"
+      + "\"subject_types_supported\":[\"public\"],"
+      + "\"id_token_signing_alg_values_supported\":[\"RS256\"]"
+      + "}";
+
+  // Discovery doc where jwks_uri is absent; Nimbus throws ParseException for 
this.
+  private static final String DISCOVERY_JSON_NO_JWKS_URI = "{"
+      + "\"issuer\":\"" + ISSUER + "\","
+      + "\"authorization_endpoint\":\"https://issuer.example.com/authorize\",";
+      + "\"response_types_supported\":[\"code\"],"
+      + "\"subject_types_supported\":[\"public\"],"
+      + "\"id_token_signing_alg_values_supported\":[\"RS256\"]"
+      + "}";
+
+  // ------------------------------------------------------------------
+  // Helpers
+  // ------------------------------------------------------------------
+
+  /** Returns a mock TrustedOidcIssuerService with fixed isTrusted / 
isDynamicJwks behavior. */
+  private static TrustedOidcIssuerService trustedDynamic() {
+    return stubService(true, true);
+  }
+
+  private static TrustedOidcIssuerService trustedStatic() {
+    return stubService(true, false);
+  }
+
+  private static TrustedOidcIssuerService untrusted() {
+    return stubService(false, false);
+  }
+
+  private static TrustedOidcIssuerService stubService(boolean trusted, boolean 
dynamicJwks) {
+    return new EmptyTrustedOidcIssuerService() {
+      @Override public boolean isTrusted(String url) { return trusted; }
+      @Override public boolean isDynamicJwks(String url) { return dynamicJwks; 
}
+    };
+  }
+
+  /**
+   * Returns a mock CloseableHttpResponse that yields the given status code 
and body.
+   * Uses a real StringEntity so EntityUtils.toString() works without deep 
mocking.
+   */
+  private static CloseableHttpResponse mockResponse(int statusCode, String 
body) throws Exception {
+    final StatusLine statusLine = EasyMock.createNiceMock(StatusLine.class);
+    
EasyMock.expect(statusLine.getStatusCode()).andReturn(statusCode).anyTimes();
+    EasyMock.replay(statusLine);
+
+    final CloseableHttpResponse response = 
EasyMock.createNiceMock(CloseableHttpResponse.class);
+    EasyMock.expect(response.getStatusLine()).andReturn(statusLine).anyTimes();
+    if (body != null) {
+      EasyMock.expect(response.getEntity()).andReturn(new StringEntity(body, 
"UTF-8")).anyTimes();
+    }
+    EasyMock.replay(response);
+    return response;
+  }
+
+  /** Returns an OIDCDiscoveryHelper backed by the given mock HttpClient. */
+  private static OIDCDiscoveryHelper helper(TrustedOidcIssuerService 
trustedIssuers,
+      CloseableHttpClient client) {
+    return new OIDCDiscoveryHelper(trustedIssuers, CACHE_TTL, client);
+  }
+
+  // ------------------------------------------------------------------
+  // SSRF gate
+  // ------------------------------------------------------------------
+
+  /**
+   * SSRF prevention: discoverJwksUri must return empty immediately for an 
issuer that is
+   * not registered for dynamic JWKS and must never call HttpClient.execute.
+   */
+  @Test
+  public void testNoHttpCallForUntrustedIssuer() throws Exception {
+    // Strict mock: any unexpected call to execute() fails the test 
immediately.
+    final CloseableHttpClient client = 
EasyMock.createMock(CloseableHttpClient.class);
+    EasyMock.replay(client);
+
+    final Optional<String> result = helper(untrusted(), 
client).discoverJwksUri(ISSUER);
+
+    assertFalse("Untrusted issuer must return empty", result.isPresent());
+    EasyMock.verify(client); // verifies execute() was never called
+  }
+
+  @Test
+  public void testStaticJwksIssuerMakesNoHttpCall() throws Exception {
+    final CloseableHttpClient client = 
EasyMock.createMock(CloseableHttpClient.class);
+    EasyMock.replay(client);
+
+    final Optional<String> result = helper(trustedStatic(), 
client).discoverJwksUri(ISSUER);
+
+    assertFalse("Static-JWKS issuer must return empty", result.isPresent());
+    EasyMock.verify(client);
+  }
+
+  // ------------------------------------------------------------------
+  // Happy path
+  // ------------------------------------------------------------------
+
+  @Test
+  public void testDiscoveryReturnsJwksUri() throws Exception {
+    final CloseableHttpClient client = 
EasyMock.createMock(CloseableHttpClient.class);
+    EasyMock.expect(client.execute(EasyMock.isA(HttpUriRequest.class)))
+        .andReturn(mockResponse(200, VALID_DISCOVERY_JSON));
+    EasyMock.replay(client);
+
+    final Optional<String> result = helper(trustedDynamic(), 
client).discoverJwksUri(ISSUER);
+
+    assertTrue(result.isPresent());
+    assertEquals(JWKS_URI, result.get());
+    EasyMock.verify(client);
+  }
+
+  // ------------------------------------------------------------------
+  // URL normalization
+  // ------------------------------------------------------------------
+
+  @Test
+  public void testDiscoveryUrlTrailingSlashStripped() throws Exception {
+    final Capture<HttpUriRequest> captured = EasyMock.newCapture();
+    final CloseableHttpClient client = 
EasyMock.createMock(CloseableHttpClient.class);
+    EasyMock.expect(client.execute(EasyMock.capture(captured)))
+        .andReturn(mockResponse(200, VALID_DISCOVERY_JSON));
+    EasyMock.replay(client);
+
+    helper(trustedDynamic(), client).discoverJwksUri(ISSUER_WITH_SLASH);
+
+    assertEquals("https://issuer.example.com/.well-known/openid-configuration";,
+        captured.getValue().getURI().toString());
+  }
+
+  @Test
+  public void testDiscoveryUrlNoDoubleSlashWithoutTrailingSlash() throws 
Exception {
+    final Capture<HttpUriRequest> captured = EasyMock.newCapture();
+    final CloseableHttpClient client = 
EasyMock.createMock(CloseableHttpClient.class);
+    EasyMock.expect(client.execute(EasyMock.capture(captured)))
+        .andReturn(mockResponse(200, VALID_DISCOVERY_JSON));
+    EasyMock.replay(client);
+
+    helper(trustedDynamic(), client).discoverJwksUri(ISSUER);
+
+    assertEquals("https://issuer.example.com/.well-known/openid-configuration";,
+        captured.getValue().getURI().toString());
+  }
+
+  // ------------------------------------------------------------------
+  // Cache behaviour
+  // ------------------------------------------------------------------
+
+  @Test
+  public void testDiscoveryDocumentCacheHit() throws Exception {
+    // Strict mock expects exactly one execute() call; a second call would 
throw.
+    final CloseableHttpClient client = 
EasyMock.createMock(CloseableHttpClient.class);
+    EasyMock.expect(client.execute(EasyMock.isA(HttpUriRequest.class)))
+        .andReturn(mockResponse(200, VALID_DISCOVERY_JSON))
+        .once();
+    EasyMock.replay(client);
+
+    final OIDCDiscoveryHelper h = helper(trustedDynamic(), client);
+    h.discoverJwksUri(ISSUER); // fetch
+    h.discoverJwksUri(ISSUER); // cache hit — must NOT call execute again
+
+    EasyMock.verify(client);
+  }
+
+  @Test
+  public void testInvalidateEvictsFromCache() throws Exception {
+    final CloseableHttpClient client = 
EasyMock.createMock(CloseableHttpClient.class);
+    EasyMock.expect(client.execute(EasyMock.isA(HttpUriRequest.class)))
+        .andReturn(mockResponse(200, VALID_DISCOVERY_JSON))
+        .times(2); // must be called twice after eviction
+    EasyMock.replay(client);
+
+    final OIDCDiscoveryHelper h = helper(trustedDynamic(), client);
+    h.discoverJwksUri(ISSUER); // fetch #1
+    h.invalidate(ISSUER);      // evict
+    h.discoverJwksUri(ISSUER); // fetch #2
+
+    EasyMock.verify(client);
+  }
+
+  /**
+   * When fetchJwksUri returns null (any failure), Caffeine must NOT cache the 
null.
+   * The next call must trigger a fresh HTTP request.
+   */
+  @Test
+  public void testNullNotCachedAfterFailure() throws Exception {
+    final CloseableHttpClient client = 
EasyMock.createMock(CloseableHttpClient.class);
+    // First call: connection error → fetchJwksUri returns null
+    EasyMock.expect(client.execute(EasyMock.isA(HttpUriRequest.class)))
+        .andThrow(new IOException("connection refused"));
+    // Second call: succeeds
+    EasyMock.expect(client.execute(EasyMock.isA(HttpUriRequest.class)))
+        .andReturn(mockResponse(200, VALID_DISCOVERY_JSON));
+    EasyMock.replay(client);
+
+    final OIDCDiscoveryHelper h = helper(trustedDynamic(), client);
+    assertFalse(h.discoverJwksUri(ISSUER).isPresent()); // failure → empty
+    assertTrue(h.discoverJwksUri(ISSUER).isPresent());  // retry → success
+
+    EasyMock.verify(client);
+  }
+
+  // ------------------------------------------------------------------
+  // HTTP error paths
+  // ------------------------------------------------------------------
+
+  @Test
+  public void testHttpGetNon200ReturnsEmpty() throws Exception {
+    final CloseableHttpClient client = 
EasyMock.createMock(CloseableHttpClient.class);
+    EasyMock.expect(client.execute(EasyMock.isA(HttpUriRequest.class)))
+        .andReturn(mockResponse(404, null));
+    EasyMock.replay(client);
+
+    assertFalse(helper(trustedDynamic(), 
client).discoverJwksUri(ISSUER).isPresent());
+    EasyMock.verify(client);
+  }
+
+  @Test
+  public void testHttpGetConnectionExceptionReturnsEmpty() throws Exception {
+    final CloseableHttpClient client = 
EasyMock.createMock(CloseableHttpClient.class);
+    EasyMock.expect(client.execute(EasyMock.isA(HttpUriRequest.class)))
+        .andThrow(new IOException("connection refused"));
+    EasyMock.replay(client);
+
+    assertFalse(helper(trustedDynamic(), 
client).discoverJwksUri(ISSUER).isPresent());
+    EasyMock.verify(client);
+  }
+
+  // ------------------------------------------------------------------
+  // Discovery document parse errors
+  // ------------------------------------------------------------------
+
+  @Test
+  public void testMalformedDiscoveryDocumentReturnsEmpty() throws Exception {
+    final CloseableHttpClient client = 
EasyMock.createMock(CloseableHttpClient.class);
+    EasyMock.expect(client.execute(EasyMock.isA(HttpUriRequest.class)))
+        .andReturn(mockResponse(200, "not valid json at all"));
+    EasyMock.replay(client);
+
+    assertFalse(helper(trustedDynamic(), 
client).discoverJwksUri(ISSUER).isPresent());
+    EasyMock.verify(client);
+  }
+
+  /**
+   * Nimbus 11.x treats jwks_uri as required and throws ParseException when it 
is absent.
+   * Verifies the catch block in fetchJwksUri handles this and returns 
Optional.empty().
+   */
+  @Test
+  public void testMissingJwksUriReturnsEmpty() throws Exception {
+    final CloseableHttpClient client = 
EasyMock.createMock(CloseableHttpClient.class);
+    EasyMock.expect(client.execute(EasyMock.isA(HttpUriRequest.class)))
+        .andReturn(mockResponse(200, DISCOVERY_JSON_NO_JWKS_URI));
+    EasyMock.replay(client);
+
+    assertFalse(helper(trustedDynamic(), 
client).discoverJwksUri(ISSUER).isPresent());
+    EasyMock.verify(client);
+  }
+}
diff --git 
a/gateway-server/src/test/java/org/apache/knox/gateway/services/knoxidf/trustedoidcissuer/TrustedOidcIssuerServiceFactoryTest.java
 
b/gateway-server/src/test/java/org/apache/knox/gateway/services/knoxidf/trustedoidcissuer/TrustedOidcIssuerServiceFactoryTest.java
new file mode 100644
index 000000000..be885e1ef
--- /dev/null
+++ 
b/gateway-server/src/test/java/org/apache/knox/gateway/services/knoxidf/trustedoidcissuer/TrustedOidcIssuerServiceFactoryTest.java
@@ -0,0 +1,265 @@
+/*
+ * 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
+ * <p>
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * <p>
+ * 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.trustedoidcissuer;
+
+import org.apache.knox.gateway.config.GatewayConfig;
+import org.apache.knox.gateway.config.impl.GatewayConfigImpl;
+import org.apache.knox.gateway.database.AbstractDataSourceFactory;
+import org.apache.knox.gateway.database.DatabaseType;
+import org.apache.knox.gateway.services.GatewayServices;
+import org.apache.knox.gateway.services.ServiceType;
+import 
org.apache.knox.gateway.services.factory.TrustedOidcIssuerServiceFactory;
+import org.apache.knox.gateway.services.security.AliasService;
+import org.apache.knox.gateway.services.topology.TopologyService;
+import org.apache.knox.gateway.topology.Topology;
+import org.easymock.EasyMock;
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+import java.sql.DriverManager;
+import java.sql.SQLException;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Map;
+
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+public class TrustedOidcIssuerServiceFactoryTest {
+
+  private static final String DB_NAME = "trustedissuers_factory_test";
+  private static final String DERBY_CREATE_URL = "jdbc:derby:memory:" + 
DB_NAME + ";create=true";
+  private static final String DERBY_SHUTDOWN_URL = "jdbc:derby:memory:" + 
DB_NAME + ";shutdown=true";
+
+  @BeforeClass
+  public static void setUpClass() throws SQLException {
+    // Derby 10.14 does not recognize locales like en_001; force a standard 
locale.
+    java.util.Locale.setDefault(java.util.Locale.US);
+    DriverManager.getConnection(DERBY_CREATE_URL).close();
+  }
+
+  @AfterClass
+  public static void tearDownClass() {
+    try {
+      DriverManager.getConnection(DERBY_SHUTDOWN_URL);
+    } catch (SQLException e) {
+      if (!(e.getErrorCode() == 45000 && "08006".equals(e.getSQLState()))) {
+        throw new RuntimeException("Unexpected Derby shutdown error", e);
+      }
+    }
+  }
+
+  // ------------------------------------------------------------------
+  // Empty (no KNOXIDF) cases
+  // ------------------------------------------------------------------
+
+  /** Zero topologies → no topology service returns anything → Empty. */
+  @Test
+  public void testNoTopologiesReturnsEmpty() throws Exception {
+    assertIsEmpty(createFactory(), buildEmptyGatewayServices(), emptyConfig());
+  }
+
+  /** Topologies exist but none contain KNOXIDF or KNOXIDF_ADMIN → Empty. */
+  @Test
+  public void testTopologiesWithNonKnoxIdfRolesReturnsEmpty() throws Exception 
{
+    final GatewayServices gws = 
buildGatewayServicesWithTopology(withRoles("HDFS", "WEBHDFS"), null);
+    assertIsEmpty(createFactory(), gws, emptyConfig());
+  }
+
+  /** TopologyService is null (not yet registered) → Empty, no NPE. */
+  @Test
+  public void testNullTopologyServiceReturnsEmpty() throws Exception {
+    final GatewayServices gws = EasyMock.createNiceMock(GatewayServices.class);
+    
EasyMock.expect(gws.getService(ServiceType.TOPOLOGY_SERVICE)).andReturn(null).anyTimes();
+    EasyMock.replay(gws);
+    assertIsEmpty(createFactory(), gws, emptyConfig());
+  }
+
+  // ------------------------------------------------------------------
+  // JDBC cases
+  // ------------------------------------------------------------------
+
+  /** A single KNOXIDF topology → JDBC. */
+  @Test
+  public void testKnoxIdfTopologyReturnsJdbc() throws Exception {
+    final GatewayServices gws = 
buildGatewayServicesWithTopology(withRoles("KNOXIDF"), derbyAlias());
+    assertIsJdbc(createFactory(), gws, derbyConfig());
+  }
+
+  /** A single KNOXIDF_ADMIN-only topology (no KNOXIDF) → JDBC. */
+  @Test
+  public void testKnoxIdfAdminOnlyTopologyReturnsJdbc() throws Exception {
+    final GatewayServices gws = 
buildGatewayServicesWithTopology(withRoles("KNOXIDF_ADMIN"), derbyAlias());
+    assertIsJdbc(createFactory(), gws, derbyConfig());
+  }
+
+  /** Both KNOXIDF and KNOXIDF_ADMIN in the same topology → JDBC. */
+  @Test
+  public void testBothRolesInSameTopologyReturnsJdbc() throws Exception {
+    final GatewayServices gws = buildGatewayServicesWithTopology(
+        withRoles("KNOXIDF", "KNOXIDF_ADMIN"), derbyAlias());
+    assertIsJdbc(createFactory(), gws, derbyConfig());
+  }
+
+  /** Multiple topologies; only the second has KNOXIDF → JDBC (verifies the 
loop continues). */
+  @Test
+  public void testMultipleTopologiesOneHasKnoxIdfReturnsJdbc() throws 
Exception {
+    final AliasService alias = derbyAlias();
+    final GatewayServices gws = buildGatewayServicesWithMultipleTopologies(
+        withRoles("HDFS", "WEBHDFS"), withRoles("KNOXIDF"), alias);
+    assertIsJdbc(createFactory(), gws, derbyConfig());
+  }
+
+  // ------------------------------------------------------------------
+  // Error handling
+  // ------------------------------------------------------------------
+
+  /**
+   * When JDBC service initialization fails (e.g. bad DB type), the factory 
must fall back
+   * to EmptyTrustedOidcIssuerService rather than propagating the exception.
+   */
+  @Test
+  public void testJdbcInitFailureFallsBackToEmpty() throws Exception {
+    final GatewayServices gws = 
buildGatewayServicesWithTopology(withRoles("KNOXIDF"), derbyAlias());
+
+    final GatewayConfig brokenConfig = 
EasyMock.createNiceMock(GatewayConfig.class);
+    
EasyMock.expect(brokenConfig.getDatabaseType()).andReturn("invalid_db_type").anyTimes();
+    EasyMock.expect(brokenConfig.getServiceParameter(EasyMock.anyString(), 
EasyMock.anyString()))
+        .andReturn("").anyTimes();
+    EasyMock.replay(brokenConfig);
+
+    assertIsEmpty(createFactory(), gws, brokenConfig);
+  }
+
+  // ------------------------------------------------------------------
+  // Helpers
+  // ------------------------------------------------------------------
+
+  private static TrustedOidcIssuerServiceFactory createFactory() {
+    return new TrustedOidcIssuerServiceFactory();
+  }
+
+  private static void assertIsEmpty(TrustedOidcIssuerServiceFactory factory,
+      GatewayServices gws, GatewayConfig config) throws Exception {
+    final org.apache.knox.gateway.services.Service result =
+        factory.create(gws, ServiceType.TRUSTED_OIDC_ISSUER_SERVICE, config, 
Map.of());
+    assertNotNull(result);
+    assertTrue("Expected EmptyTrustedOidcIssuerService but got " + 
result.getClass().getSimpleName(),
+        result instanceof EmptyTrustedOidcIssuerService);
+  }
+
+  private static void assertIsJdbc(TrustedOidcIssuerServiceFactory factory,
+      GatewayServices gws, GatewayConfig config) throws Exception {
+    final org.apache.knox.gateway.services.Service result =
+        factory.create(gws, ServiceType.TRUSTED_OIDC_ISSUER_SERVICE, config, 
Map.of());
+    assertNotNull(result);
+    assertTrue("Expected JdbcTrustedOidcIssuerService but got " + 
result.getClass().getSimpleName(),
+        result instanceof JdbcTrustedOidcIssuerService);
+  }
+
+  /** GatewayServices with a TopologyService returning no topologies; no 
AliasService needed. */
+  private static GatewayServices buildEmptyGatewayServices() {
+    final TopologyService topologyService = 
EasyMock.createNiceMock(TopologyService.class);
+    
EasyMock.expect(topologyService.getTopologies()).andReturn(Collections.emptyList()).anyTimes();
+    EasyMock.replay(topologyService);
+
+    final GatewayServices gws = EasyMock.createNiceMock(GatewayServices.class);
+    EasyMock.expect(gws.getService(ServiceType.TOPOLOGY_SERVICE))
+        .andReturn(topologyService).anyTimes();
+    EasyMock.replay(gws);
+    return gws;
+  }
+
+  /**
+   * Builds a {@link GatewayServices} mock with one topology that has the 
given service roles.
+   * {@code alias} may be null when no JDBC init will be attempted.
+   */
+  private static GatewayServices buildGatewayServicesWithTopology(
+      String[] roles, AliasService alias) throws Exception {
+    final Topology topology = topologyWithRoles(roles);
+    return buildGatewayServices(Collections.singletonList(topology), alias);
+  }
+
+  private static GatewayServices buildGatewayServicesWithMultipleTopologies(
+      String[] roles1, String[] roles2, AliasService alias) throws Exception {
+    return buildGatewayServices(
+        Arrays.asList(topologyWithRoles(roles1), topologyWithRoles(roles2)), 
alias);
+  }
+
+  private static GatewayServices buildGatewayServices(
+      java.util.List<Topology> topologies, AliasService alias) throws 
Exception {
+    final TopologyService topologyService = 
EasyMock.createNiceMock(TopologyService.class);
+    
EasyMock.expect(topologyService.getTopologies()).andReturn(topologies).anyTimes();
+    EasyMock.replay(topologyService);
+
+    final GatewayServices gws = EasyMock.createNiceMock(GatewayServices.class);
+    EasyMock.expect(gws.getService(ServiceType.TOPOLOGY_SERVICE))
+        .andReturn(topologyService).anyTimes();
+    if (alias != null) {
+      EasyMock.expect(gws.getService(ServiceType.ALIAS_SERVICE))
+          .andReturn(alias).anyTimes();
+    }
+    EasyMock.replay(gws);
+    return gws;
+  }
+
+  private static Topology topologyWithRoles(String... roles) {
+    final Topology topology = EasyMock.createNiceMock(Topology.class);
+    final java.util.List<org.apache.knox.gateway.topology.Service> services = 
new java.util.ArrayList<>();
+    for (String role : roles) {
+      final org.apache.knox.gateway.topology.Service svc =
+          
EasyMock.createNiceMock(org.apache.knox.gateway.topology.Service.class);
+      EasyMock.expect(svc.getRole()).andReturn(role).anyTimes();
+      EasyMock.replay(svc);
+      services.add(svc);
+    }
+    EasyMock.expect(topology.getServices()).andReturn(services).anyTimes();
+    EasyMock.replay(topology);
+    return topology;
+  }
+
+  private static String[] withRoles(String... roles) {
+    return roles;
+  }
+
+  private static AliasService derbyAlias() throws Exception {
+    final AliasService alias = EasyMock.createNiceMock(AliasService.class);
+    EasyMock.expect(alias.getPasswordFromAliasForGateway(
+        
AbstractDataSourceFactory.DATABASE_USER_ALIAS_NAME)).andReturn(null).anyTimes();
+    EasyMock.expect(alias.getPasswordFromAliasForGateway(
+        
AbstractDataSourceFactory.DATABASE_PASSWORD_ALIAS_NAME)).andReturn(null).anyTimes();
+    EasyMock.replay(alias);
+    return alias;
+  }
+
+  private static GatewayConfig derbyConfig() {
+    final GatewayConfigImpl config = new GatewayConfigImpl();
+    config.set(GatewayConfigImpl.GATEWAY_DATABASE_TYPE, 
DatabaseType.DERBY.type());
+    config.set(GatewayConfigImpl.GATEWAY_DATABASE_NAME, "memory:" + DB_NAME);
+    return config;
+  }
+
+  /** Config mock that returns empty string for getServiceParameter (required 
for impl detection). */
+  private static GatewayConfig emptyConfig() {
+    final GatewayConfig config = EasyMock.createNiceMock(GatewayConfig.class);
+    EasyMock.expect(config.getServiceParameter(EasyMock.anyString(), 
EasyMock.anyString()))
+        .andReturn("").anyTimes();
+    EasyMock.replay(config);
+    return config;
+  }
+}
diff --git 
a/gateway-server/src/test/java/org/apache/knox/gateway/services/knoxidf/trustedoidcissuer/TrustedOidcIssuersSchemaTest.java
 
b/gateway-server/src/test/java/org/apache/knox/gateway/services/knoxidf/trustedoidcissuer/TrustedOidcIssuersSchemaTest.java
index e8cb56015..8645017cc 100644
--- 
a/gateway-server/src/test/java/org/apache/knox/gateway/services/knoxidf/trustedoidcissuer/TrustedOidcIssuersSchemaTest.java
+++ 
b/gateway-server/src/test/java/org/apache/knox/gateway/services/knoxidf/trustedoidcissuer/TrustedOidcIssuersSchemaTest.java
@@ -53,6 +53,8 @@ public class TrustedOidcIssuersSchemaTest {
 
   @BeforeClass
   public static void setUp() throws SQLException {
+    // Derby 10.14 does not recognize locales like en_001; force a standard 
locale.
+    java.util.Locale.setDefault(java.util.Locale.US);
     derbyConn = DriverManager.getConnection(DERBY_URL);
     hsqlConn = DriverManager.getConnection(HSQL_URL, HSQL_USER, HSQL_PASSWORD);
   }
diff --git 
a/gateway-util-common/src/main/java/org/apache/knox/gateway/util/knoxidf/KnoxIDFConstants.java
 
b/gateway-util-common/src/main/java/org/apache/knox/gateway/util/knoxidf/KnoxIDFConstants.java
index 5573b90e2..99ba8b5e3 100644
--- 
a/gateway-util-common/src/main/java/org/apache/knox/gateway/util/knoxidf/KnoxIDFConstants.java
+++ 
b/gateway-util-common/src/main/java/org/apache/knox/gateway/util/knoxidf/KnoxIDFConstants.java
@@ -57,4 +57,17 @@ public interface KnoxIDFConstants {
     String FEDERATED_OP_CONFIG_NAMES = FEDERATED_OP_CONFIG_PREFIX + "names";
 
     String TOKEN_EXCHANGE_TOPOLOGY_NAME = "token.exchange.topology.name";
+
+    // TrustedOidcIssuerService gateway-level params (read from GatewayConfig 
/ gateway-site.xml)
+    String TRUSTED_OIDC_ISSUER_DISCOVERY_CACHE_TTL_SECS =
+        "gateway.trustedoidcissuer.discovery.cache.ttl.secs";
+    String TRUSTED_OIDC_ISSUER_DISCOVERY_CONNECT_TIMEOUT_MS =
+        "gateway.trustedoidcissuer.discovery.connect.timeout.ms";
+    String TRUSTED_OIDC_ISSUER_DISCOVERY_READ_TIMEOUT_MS =
+        "gateway.trustedoidcissuer.discovery.read.timeout.ms";
+
+    // Default values for gateway-level TrustedOidcIssuerService params
+    int TRUSTED_OIDC_ISSUER_DEFAULT_DISCOVERY_CACHE_TTL_SECS = 600;
+    int TRUSTED_OIDC_ISSUER_DEFAULT_DISCOVERY_CONNECT_TIMEOUT_MS = 3000;
+    int TRUSTED_OIDC_ISSUER_DEFAULT_DISCOVERY_READ_TIMEOUT_MS = 10000;
 }
diff --git a/pom.xml b/pom.xml
index 52a60c643..99b9c803e 100644
--- a/pom.xml
+++ b/pom.xml
@@ -259,6 +259,7 @@
         <mina.version>2.2.8</mina.version>
         <netty.version>4.1.127.Final</netty.version>
         <nimbus-jose-jwt.version>10.9.1</nimbus-jose-jwt.version>
+        <oauth2-oidc-sdk.version>11.37.2</oauth2-oidc-sdk.version>
         <nodejs.version>v22.20.0</nodejs.version>
         <okhttp.version>4.12.0</okhttp.version>
         <opensaml.version>5.2.2</opensaml.version>
@@ -1517,6 +1518,11 @@
                 <artifactId>nimbus-jose-jwt</artifactId>
                 <version>${nimbus-jose-jwt.version}</version>
             </dependency>
+            <dependency>
+                <groupId>com.nimbusds</groupId>
+                <artifactId>oauth2-oidc-sdk</artifactId>
+                <version>${oauth2-oidc-sdk.version}</version>
+            </dependency>
 
             <dependency>
                 <groupId>net.minidev</groupId>


Reply via email to