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

honahx pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/polaris.git


The following commit(s) were added to refs/heads/main by this push:
     new 6b957ec14 [Catalog Federation] Add Connection Credential Vendors for 
Other Auth Types (#2782)
6b957ec14 is described below

commit 6b957ec14516ab764783833c4e5c40ba58fe3864
Author: Rulin Xing <[email protected]>
AuthorDate: Tue Oct 14 13:00:24 2025 -0700

    [Catalog Federation] Add Connection Credential Vendors for Other Auth Types 
(#2782)
    
    Add Connection Credential Vendors for Other Auth Types
    
    This change is a prerequisite for enabling connection credential caching.
    By making PolarisCredentialManager the central entry point for obtaining 
connection credentials, we can introduce caching cleanly and manage all 
credential flows in a consistent way.
---
 .../hadoop/HadoopFederatedCatalogFactory.java      |   8 +-
 .../hive/HiveFederatedCatalogFactory.java          |   8 +-
 .../core/catalog/ExternalCatalogFactory.java       |  13 +--
 .../BearerAuthenticationParametersDpo.java         |  11 --
 .../ImplicitAuthenticationParametersDpo.java       |   5 +-
 .../OAuthClientCredentialsParametersDpo.java       |  11 +-
 .../SigV4AuthenticationParametersDpo.java          |   5 +-
 .../hadoop/HadoopConnectionConfigInfoDpo.java      |   9 +-
 .../hive/HiveConnectionConfigInfoDpo.java          |   8 +-
 .../iceberg/IcebergCatalogPropertiesProvider.java  |   7 +-
 .../IcebergRestConnectionConfigInfoDpo.java        |   9 +-
 .../connection/CatalogAccessProperty.java          |  43 ++++++--
 .../connection/ConnectionCredentials.java          |   6 +-
 .../service/catalog/common/CatalogHandler.java     |   8 --
 .../generic/GenericTableCatalogAdapter.java        |   5 -
 .../generic/GenericTableCatalogHandler.java        |   5 +-
 .../catalog/iceberg/IcebergCatalogAdapter.java     |   5 -
 .../catalog/iceberg/IcebergCatalogHandler.java     |   8 +-
 .../iceberg/IcebergRESTExternalCatalogFactory.java |   9 +-
 .../catalog/policy/PolicyCatalogAdapter.java       |   5 -
 .../catalog/policy/PolicyCatalogHandler.java       |   3 -
 .../service/config/ProductionReadinessChecks.java  |  62 +++++++++++
 .../credentials/CredentialVendorPriorities.java    |  26 ++---
 .../service/credentials/connection/AuthType.java   |   2 +-
 .../BearerConnectionCredentialVendor.java          |  85 +++++++++++++++
 .../ImplicitConnectionCredentialVendor.java        |  68 ++++++++++++
 .../connection/OAuthClientCredentialVendor.java    |  92 ++++++++++++++++
 .../SigV4ConnectionCredentialVendor.java           |  30 +++---
 ...PolarisGenericTableCatalogHandlerAuthzTest.java |   1 -
 .../iceberg/IcebergCatalogHandlerAuthzTest.java    |   4 -
 ...ebergCatalogHandlerFineGrainedDisabledTest.java |   1 -
 .../policy/PolicyCatalogHandlerAuthzTest.java      |   1 -
 .../BearerConnectionCredentialVendorTest.java      | 109 +++++++++++++++++++
 .../ImplicitConnectionCredentialVendorTest.java    |  74 +++++++++++++
 .../OAuthClientCredentialVendorTest.java           | 116 +++++++++++++++++++++
 .../org/apache/polaris/service/TestServices.java   |   1 -
 36 files changed, 708 insertions(+), 155 deletions(-)

diff --git 
a/extensions/federation/hadoop/src/main/java/org/apache/polaris/extensions/federation/hadoop/HadoopFederatedCatalogFactory.java
 
b/extensions/federation/hadoop/src/main/java/org/apache/polaris/extensions/federation/hadoop/HadoopFederatedCatalogFactory.java
index 95bd26f9d..b2cc24ec1 100644
--- 
a/extensions/federation/hadoop/src/main/java/org/apache/polaris/extensions/federation/hadoop/HadoopFederatedCatalogFactory.java
+++ 
b/extensions/federation/hadoop/src/main/java/org/apache/polaris/extensions/federation/hadoop/HadoopFederatedCatalogFactory.java
@@ -31,7 +31,6 @@ import 
org.apache.polaris.core.connection.ConnectionConfigInfoDpo;
 import org.apache.polaris.core.connection.ConnectionType;
 import org.apache.polaris.core.connection.hadoop.HadoopConnectionConfigInfoDpo;
 import org.apache.polaris.core.credentials.PolarisCredentialManager;
-import org.apache.polaris.core.secrets.UserSecretsManager;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -44,7 +43,6 @@ public class HadoopFederatedCatalogFactory implements 
ExternalCatalogFactory {
   @Override
   public Catalog createCatalog(
       ConnectionConfigInfoDpo connectionConfigInfoDpo,
-      UserSecretsManager userSecretsManager,
       PolarisCredentialManager polarisCredentialManager) {
     // Currently, Polaris supports Hadoop federation only via IMPLICIT 
authentication.
     // Hence, prior to initializing the configuration, ensure that the catalog 
uses
@@ -59,15 +57,13 @@ public class HadoopFederatedCatalogFactory implements 
ExternalCatalogFactory {
     String warehouse = ((HadoopConnectionConfigInfoDpo) 
connectionConfigInfoDpo).getWarehouse();
     HadoopCatalog hadoopCatalog = new HadoopCatalog(conf, warehouse);
     hadoopCatalog.initialize(
-        warehouse,
-        connectionConfigInfoDpo.asIcebergCatalogProperties(
-            userSecretsManager, polarisCredentialManager));
+        warehouse, 
connectionConfigInfoDpo.asIcebergCatalogProperties(polarisCredentialManager));
     return hadoopCatalog;
   }
 
   @Override
   public GenericTableCatalog createGenericCatalog(
-      ConnectionConfigInfoDpo connectionConfig, UserSecretsManager 
userSecretsManager) {
+      ConnectionConfigInfoDpo connectionConfig, PolarisCredentialManager 
polarisCredentialManager) {
     // TODO implement
     throw new UnsupportedOperationException(
         "Generic table federation to this catalog is not supported.");
diff --git 
a/extensions/federation/hive/src/main/java/org/apache/polaris/extensions/federation/hive/HiveFederatedCatalogFactory.java
 
b/extensions/federation/hive/src/main/java/org/apache/polaris/extensions/federation/hive/HiveFederatedCatalogFactory.java
index 0f88acf09..939bc5384 100644
--- 
a/extensions/federation/hive/src/main/java/org/apache/polaris/extensions/federation/hive/HiveFederatedCatalogFactory.java
+++ 
b/extensions/federation/hive/src/main/java/org/apache/polaris/extensions/federation/hive/HiveFederatedCatalogFactory.java
@@ -30,7 +30,6 @@ import 
org.apache.polaris.core.connection.ConnectionConfigInfoDpo;
 import org.apache.polaris.core.connection.ConnectionType;
 import org.apache.polaris.core.connection.hive.HiveConnectionConfigInfoDpo;
 import org.apache.polaris.core.credentials.PolarisCredentialManager;
-import org.apache.polaris.core.secrets.UserSecretsManager;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -43,7 +42,6 @@ public class HiveFederatedCatalogFactory implements 
ExternalCatalogFactory {
   @Override
   public Catalog createCatalog(
       ConnectionConfigInfoDpo connectionConfigInfoDpo,
-      UserSecretsManager userSecretsManager,
       PolarisCredentialManager polarisCredentialManager) {
     // Currently, Polaris supports Hive federation only via IMPLICIT 
authentication.
     // Hence, prior to initializing the configuration, ensure that the catalog 
uses
@@ -72,15 +70,13 @@ public class HiveFederatedCatalogFactory implements 
ExternalCatalogFactory {
     // Kerberos instances are not suitable because Kerberos ties a single 
identity to the server.
     HiveCatalog hiveCatalog = new HiveCatalog();
     hiveCatalog.initialize(
-        warehouse,
-        connectionConfigInfoDpo.asIcebergCatalogProperties(
-            userSecretsManager, polarisCredentialManager));
+        warehouse, 
connectionConfigInfoDpo.asIcebergCatalogProperties(polarisCredentialManager));
     return hiveCatalog;
   }
 
   @Override
   public GenericTableCatalog createGenericCatalog(
-      ConnectionConfigInfoDpo connectionConfig, UserSecretsManager 
userSecretsManager) {
+      ConnectionConfigInfoDpo connectionConfig, PolarisCredentialManager 
polarisCredentialManager) {
     // TODO implement
     throw new UnsupportedOperationException(
         "Generic table federation to this catalog is not supported.");
diff --git 
a/polaris-core/src/main/java/org/apache/polaris/core/catalog/ExternalCatalogFactory.java
 
b/polaris-core/src/main/java/org/apache/polaris/core/catalog/ExternalCatalogFactory.java
index 035563b52..6253b8809 100644
--- 
a/polaris-core/src/main/java/org/apache/polaris/core/catalog/ExternalCatalogFactory.java
+++ 
b/polaris-core/src/main/java/org/apache/polaris/core/catalog/ExternalCatalogFactory.java
@@ -21,7 +21,6 @@ package org.apache.polaris.core.catalog;
 import org.apache.iceberg.catalog.Catalog;
 import org.apache.polaris.core.connection.ConnectionConfigInfoDpo;
 import org.apache.polaris.core.credentials.PolarisCredentialManager;
-import org.apache.polaris.core.secrets.UserSecretsManager;
 
 /**
  * Factory interface for creating external catalog handles based on connection 
configuration.
@@ -35,25 +34,23 @@ public interface ExternalCatalogFactory {
    * Creates a catalog handle for the given connection configuration.
    *
    * @param connectionConfig the connection configuration
-   * @param userSecretsManager the user secrets manager for handling 
user-provided credentials
-   * @param polarisCredentialManager the credential manager for generating 
temporary credentials
+   * @param polarisCredentialManager the credential manager for generating 
connection credentials
    *     that Polaris uses to access external systems
    * @return the initialized catalog
    * @throws IllegalStateException if the connection configuration is invalid
    */
   Catalog createCatalog(
-      ConnectionConfigInfoDpo connectionConfig,
-      UserSecretsManager userSecretsManager,
-      PolarisCredentialManager polarisCredentialManager);
+      ConnectionConfigInfoDpo connectionConfig, PolarisCredentialManager 
polarisCredentialManager);
 
   /**
    * Creates a generic table catalog for the given connection configuration.
    *
    * @param connectionConfig the connection configuration
-   * @param userSecretsManager the user secrets manager for handling 
credentials
+   * @param polarisCredentialManager the credential manager for generating 
connection credentials
+   *     that Polaris uses to access external systems
    * @return the initialized catalog
    * @throws IllegalStateException if the connection configuration is invalid
    */
   GenericTableCatalog createGenericCatalog(
-      ConnectionConfigInfoDpo connectionConfig, UserSecretsManager 
userSecretsManager);
+      ConnectionConfigInfoDpo connectionConfig, PolarisCredentialManager 
polarisCredentialManager);
 }
diff --git 
a/polaris-core/src/main/java/org/apache/polaris/core/connection/BearerAuthenticationParametersDpo.java
 
b/polaris-core/src/main/java/org/apache/polaris/core/connection/BearerAuthenticationParametersDpo.java
index c11502015..2d06b22cf 100644
--- 
a/polaris-core/src/main/java/org/apache/polaris/core/connection/BearerAuthenticationParametersDpo.java
+++ 
b/polaris-core/src/main/java/org/apache/polaris/core/connection/BearerAuthenticationParametersDpo.java
@@ -21,13 +21,9 @@ package org.apache.polaris.core.connection;
 import com.fasterxml.jackson.annotation.JsonProperty;
 import com.google.common.base.MoreObjects;
 import jakarta.annotation.Nonnull;
-import java.util.Map;
-import org.apache.iceberg.rest.auth.OAuth2Properties;
 import org.apache.polaris.core.admin.model.AuthenticationParameters;
 import org.apache.polaris.core.admin.model.BearerAuthenticationParameters;
-import org.apache.polaris.core.credentials.PolarisCredentialManager;
 import org.apache.polaris.core.secrets.SecretReference;
-import org.apache.polaris.core.secrets.UserSecretsManager;
 
 /**
  * The internal persistence-object counterpart to 
BearerAuthenticationParameters defined in the API
@@ -49,13 +45,6 @@ public class BearerAuthenticationParametersDpo extends 
AuthenticationParametersD
     return bearerTokenReference;
   }
 
-  @Override
-  public @Nonnull Map<String, String> asIcebergCatalogProperties(
-      UserSecretsManager secretsManager, PolarisCredentialManager 
credentialManager) {
-    String bearerToken = secretsManager.readSecret(getBearerTokenReference());
-    return Map.of(OAuth2Properties.TOKEN, bearerToken);
-  }
-
   @Override
   public @Nonnull AuthenticationParameters asAuthenticationParametersModel() {
     return BearerAuthenticationParameters.builder()
diff --git 
a/polaris-core/src/main/java/org/apache/polaris/core/connection/ImplicitAuthenticationParametersDpo.java
 
b/polaris-core/src/main/java/org/apache/polaris/core/connection/ImplicitAuthenticationParametersDpo.java
index 113872429..371c9766b 100644
--- 
a/polaris-core/src/main/java/org/apache/polaris/core/connection/ImplicitAuthenticationParametersDpo.java
+++ 
b/polaris-core/src/main/java/org/apache/polaris/core/connection/ImplicitAuthenticationParametersDpo.java
@@ -24,7 +24,6 @@ import java.util.Map;
 import org.apache.polaris.core.admin.model.AuthenticationParameters;
 import org.apache.polaris.core.admin.model.ImplicitAuthenticationParameters;
 import org.apache.polaris.core.credentials.PolarisCredentialManager;
-import org.apache.polaris.core.secrets.UserSecretsManager;
 
 /**
  * The internal persistence-object counterpart to 
ImplicitAuthenticationParameters defined in the
@@ -38,7 +37,9 @@ public class ImplicitAuthenticationParametersDpo extends 
AuthenticationParameter
 
   @Override
   public @Nonnull Map<String, String> asIcebergCatalogProperties(
-      UserSecretsManager secretsManager, PolarisCredentialManager 
credentialManager) {
+      PolarisCredentialManager credentialManager) {
+    // Return only metadata properties - credentials are handled by 
ConnectionCredentialVendor
+    // Implicit auth has no metadata properties
     return Map.of();
   }
 
diff --git 
a/polaris-core/src/main/java/org/apache/polaris/core/connection/OAuthClientCredentialsParametersDpo.java
 
b/polaris-core/src/main/java/org/apache/polaris/core/connection/OAuthClientCredentialsParametersDpo.java
index 9c1293624..7b855496b 100644
--- 
a/polaris-core/src/main/java/org/apache/polaris/core/connection/OAuthClientCredentialsParametersDpo.java
+++ 
b/polaris-core/src/main/java/org/apache/polaris/core/connection/OAuthClientCredentialsParametersDpo.java
@@ -37,7 +37,6 @@ import 
org.apache.polaris.core.admin.model.AuthenticationParameters;
 import org.apache.polaris.core.admin.model.OAuthClientCredentialsParameters;
 import org.apache.polaris.core.credentials.PolarisCredentialManager;
 import org.apache.polaris.core.secrets.SecretReference;
-import org.apache.polaris.core.secrets.UserSecretsManager;
 
 /**
  * The internal persistence-object counterpart to 
OAuthClientCredentialsParameters defined in the
@@ -97,20 +96,14 @@ public class OAuthClientCredentialsParametersDpo extends 
AuthenticationParameter
         Objects.requireNonNullElse(scopes, 
List.of(OAuth2Properties.CATALOG_SCOPE)));
   }
 
-  @JsonIgnore
-  private @Nonnull String getCredentialAsConcatenatedString(UserSecretsManager 
secretsManager) {
-    String clientSecret = 
secretsManager.readSecret(getClientSecretReference());
-    return COLON_JOINER.join(clientId, clientSecret);
-  }
-
   @Override
   public @Nonnull Map<String, String> asIcebergCatalogProperties(
-      UserSecretsManager secretsManager, PolarisCredentialManager 
credentialManager) {
+      PolarisCredentialManager credentialManager) {
+    // Return only metadata properties - credentials are handled by 
ConnectionCredentialVendor
     HashMap<String, String> properties = new HashMap<>();
     if (getTokenUri() != null) {
       properties.put(OAuth2Properties.OAUTH2_SERVER_URI, getTokenUri());
     }
-    properties.put(OAuth2Properties.CREDENTIAL, 
getCredentialAsConcatenatedString(secretsManager));
     properties.put(OAuth2Properties.SCOPE, getScopesAsString());
     return properties;
   }
diff --git 
a/polaris-core/src/main/java/org/apache/polaris/core/connection/SigV4AuthenticationParametersDpo.java
 
b/polaris-core/src/main/java/org/apache/polaris/core/connection/SigV4AuthenticationParametersDpo.java
index 061245110..16dfd6e58 100644
--- 
a/polaris-core/src/main/java/org/apache/polaris/core/connection/SigV4AuthenticationParametersDpo.java
+++ 
b/polaris-core/src/main/java/org/apache/polaris/core/connection/SigV4AuthenticationParametersDpo.java
@@ -29,7 +29,6 @@ import org.apache.iceberg.rest.auth.AuthProperties;
 import org.apache.polaris.core.admin.model.AuthenticationParameters;
 import org.apache.polaris.core.admin.model.SigV4AuthenticationParameters;
 import org.apache.polaris.core.credentials.PolarisCredentialManager;
-import org.apache.polaris.core.secrets.UserSecretsManager;
 
 /**
  * The internal persistence-object counterpart to 
SigV4AuthenticationParameters defined in the API
@@ -95,14 +94,14 @@ public class SigV4AuthenticationParametersDpo extends 
AuthenticationParametersDp
   @Nonnull
   @Override
   public Map<String, String> asIcebergCatalogProperties(
-      UserSecretsManager secretsManager, PolarisCredentialManager 
credentialManager) {
+      PolarisCredentialManager credentialManager) {
+    // Return only metadata properties - credentials are handled by 
ConnectionCredentialVendor
     ImmutableMap.Builder<String, String> builder = ImmutableMap.builder();
     builder.put(AuthProperties.AUTH_TYPE, AuthProperties.AUTH_TYPE_SIGV4);
     builder.put(AwsProperties.REST_SIGNER_REGION, getSigningRegion());
     if (getSigningName() != null) {
       builder.put(AwsProperties.REST_SIGNING_NAME, getSigningName());
     }
-    // Connection credentials are handled by ConnectionConfigInfoDpo
     return builder.build();
   }
 
diff --git 
a/polaris-core/src/main/java/org/apache/polaris/core/connection/hadoop/HadoopConnectionConfigInfoDpo.java
 
b/polaris-core/src/main/java/org/apache/polaris/core/connection/hadoop/HadoopConnectionConfigInfoDpo.java
index 26be6ad83..accbc7be1 100644
--- 
a/polaris-core/src/main/java/org/apache/polaris/core/connection/hadoop/HadoopConnectionConfigInfoDpo.java
+++ 
b/polaris-core/src/main/java/org/apache/polaris/core/connection/hadoop/HadoopConnectionConfigInfoDpo.java
@@ -35,7 +35,6 @@ import 
org.apache.polaris.core.credentials.PolarisCredentialManager;
 import org.apache.polaris.core.credentials.connection.ConnectionCredentials;
 import org.apache.polaris.core.identity.dpo.ServiceIdentityInfoDpo;
 import org.apache.polaris.core.identity.provider.ServiceIdentityProvider;
-import org.apache.polaris.core.secrets.UserSecretsManager;
 
 /**
  * The internal persistence-object counterpart to {@link
@@ -73,16 +72,14 @@ public class HadoopConnectionConfigInfoDpo extends 
ConnectionConfigInfoDpo {
 
   @Override
   public @Nonnull Map<String, String> asIcebergCatalogProperties(
-      UserSecretsManager secretsManager, PolarisCredentialManager 
credentialManager) {
+      PolarisCredentialManager credentialManager) {
     HashMap<String, String> properties = new HashMap<>();
     properties.put(CatalogProperties.URI, getUri());
     if (getWarehouse() != null) {
       properties.put(CatalogProperties.WAREHOUSE_LOCATION, getWarehouse());
     }
-    // Add authentication-specific properties
-    properties.putAll(
-        getAuthenticationParameters()
-            .asIcebergCatalogProperties(secretsManager, credentialManager));
+    // Add authentication-specific metadata (non-credential properties)
+    
properties.putAll(getAuthenticationParameters().asIcebergCatalogProperties(credentialManager));
     // Add connection credentials from Polaris credential manager
     ConnectionCredentials connectionCredentials = 
credentialManager.getConnectionCredentials(this);
     properties.putAll(connectionCredentials.credentials());
diff --git 
a/polaris-core/src/main/java/org/apache/polaris/core/connection/hive/HiveConnectionConfigInfoDpo.java
 
b/polaris-core/src/main/java/org/apache/polaris/core/connection/hive/HiveConnectionConfigInfoDpo.java
index d4a7e2b3d..51619216e 100644
--- 
a/polaris-core/src/main/java/org/apache/polaris/core/connection/hive/HiveConnectionConfigInfoDpo.java
+++ 
b/polaris-core/src/main/java/org/apache/polaris/core/connection/hive/HiveConnectionConfigInfoDpo.java
@@ -35,7 +35,6 @@ import 
org.apache.polaris.core.credentials.PolarisCredentialManager;
 import org.apache.polaris.core.credentials.connection.ConnectionCredentials;
 import org.apache.polaris.core.identity.dpo.ServiceIdentityInfoDpo;
 import org.apache.polaris.core.identity.provider.ServiceIdentityProvider;
-import org.apache.polaris.core.secrets.UserSecretsManager;
 
 /**
  * The internal persistence-object counterpart to {@link
@@ -72,17 +71,16 @@ public class HiveConnectionConfigInfoDpo extends 
ConnectionConfigInfoDpo {
 
   @Override
   public @Nonnull Map<String, String> asIcebergCatalogProperties(
-      UserSecretsManager secretsManager, PolarisCredentialManager 
polarisCredentialManager) {
+      PolarisCredentialManager polarisCredentialManager) {
     HashMap<String, String> properties = new HashMap<>();
     properties.put(CatalogProperties.URI, getUri());
     if (getWarehouse() != null) {
       properties.put(CatalogProperties.WAREHOUSE_LOCATION, getWarehouse());
     }
     if (getAuthenticationParameters() != null) {
-      // Add authentication-specific properties
+      // Add authentication-specific metadata (non-credential properties)
       properties.putAll(
-          getAuthenticationParameters()
-              .asIcebergCatalogProperties(secretsManager, 
polarisCredentialManager));
+          
getAuthenticationParameters().asIcebergCatalogProperties(polarisCredentialManager));
       // Add connection credentials from Polaris credential manager
       ConnectionCredentials connectionCredentials =
           polarisCredentialManager.getConnectionCredentials(this);
diff --git 
a/polaris-core/src/main/java/org/apache/polaris/core/connection/iceberg/IcebergCatalogPropertiesProvider.java
 
b/polaris-core/src/main/java/org/apache/polaris/core/connection/iceberg/IcebergCatalogPropertiesProvider.java
index e17218f25..c07ceaabb 100644
--- 
a/polaris-core/src/main/java/org/apache/polaris/core/connection/iceberg/IcebergCatalogPropertiesProvider.java
+++ 
b/polaris-core/src/main/java/org/apache/polaris/core/connection/iceberg/IcebergCatalogPropertiesProvider.java
@@ -21,7 +21,6 @@ package org.apache.polaris.core.connection.iceberg;
 import jakarta.annotation.Nonnull;
 import java.util.Map;
 import org.apache.polaris.core.credentials.PolarisCredentialManager;
-import org.apache.polaris.core.secrets.UserSecretsManager;
 
 /**
  * Configuration wrappers which ultimately translate their contents into 
Iceberg properties and
@@ -31,6 +30,8 @@ import org.apache.polaris.core.secrets.UserSecretsManager;
  */
 public interface IcebergCatalogPropertiesProvider {
   @Nonnull
-  Map<String, String> asIcebergCatalogProperties(
-      UserSecretsManager secretsManager, PolarisCredentialManager 
credentialManager);
+  default Map<String, String> asIcebergCatalogProperties(
+      PolarisCredentialManager credentialManager) {
+    return Map.of();
+  }
 }
diff --git 
a/polaris-core/src/main/java/org/apache/polaris/core/connection/iceberg/IcebergRestConnectionConfigInfoDpo.java
 
b/polaris-core/src/main/java/org/apache/polaris/core/connection/iceberg/IcebergRestConnectionConfigInfoDpo.java
index 43f5c8a92..56e3144fd 100644
--- 
a/polaris-core/src/main/java/org/apache/polaris/core/connection/iceberg/IcebergRestConnectionConfigInfoDpo.java
+++ 
b/polaris-core/src/main/java/org/apache/polaris/core/connection/iceberg/IcebergRestConnectionConfigInfoDpo.java
@@ -35,7 +35,6 @@ import 
org.apache.polaris.core.credentials.PolarisCredentialManager;
 import org.apache.polaris.core.credentials.connection.ConnectionCredentials;
 import org.apache.polaris.core.identity.dpo.ServiceIdentityInfoDpo;
 import org.apache.polaris.core.identity.provider.ServiceIdentityProvider;
-import org.apache.polaris.core.secrets.UserSecretsManager;
 
 /**
  * The internal persistence-object counterpart to 
IcebergRestConnectionConfigInfo defined in the API
@@ -65,16 +64,14 @@ public class IcebergRestConnectionConfigInfoDpo extends 
ConnectionConfigInfoDpo
 
   @Override
   public @Nonnull Map<String, String> asIcebergCatalogProperties(
-      UserSecretsManager secretsManager, PolarisCredentialManager 
credentialManager) {
+      PolarisCredentialManager credentialManager) {
     HashMap<String, String> properties = new HashMap<>();
     properties.put(CatalogProperties.URI, getUri());
     if (getRemoteCatalogName() != null) {
       properties.put(CatalogProperties.WAREHOUSE_LOCATION, 
getRemoteCatalogName());
     }
-    // Add authentication-specific properties
-    properties.putAll(
-        getAuthenticationParameters()
-            .asIcebergCatalogProperties(secretsManager, credentialManager));
+    // Add authentication-specific metadata (non-credential properties)
+    
properties.putAll(getAuthenticationParameters().asIcebergCatalogProperties(credentialManager));
     // Add connection credentials from Polaris credential manager
     ConnectionCredentials connectionCredentials = 
credentialManager.getConnectionCredentials(this);
     properties.putAll(connectionCredentials.credentials());
diff --git 
a/polaris-core/src/main/java/org/apache/polaris/core/credentials/connection/CatalogAccessProperty.java
 
b/polaris-core/src/main/java/org/apache/polaris/core/credentials/connection/CatalogAccessProperty.java
index cb42f7e32..a683e61bf 100644
--- 
a/polaris-core/src/main/java/org/apache/polaris/core/credentials/connection/CatalogAccessProperty.java
+++ 
b/polaris-core/src/main/java/org/apache/polaris/core/credentials/connection/CatalogAccessProperty.java
@@ -20,6 +20,7 @@
 package org.apache.polaris.core.credentials.connection;
 
 import org.apache.iceberg.aws.AwsProperties;
+import org.apache.iceberg.rest.auth.OAuth2Properties;
 
 /**
  * A subset of Iceberg catalog properties recognized by Polaris.
@@ -28,28 +29,54 @@ import org.apache.iceberg.aws.AwsProperties;
  * Catalog service.
  */
 public enum CatalogAccessProperty {
+  // OAuth
+  OAUTH2_CREDENTIAL(String.class, OAuth2Properties.CREDENTIAL, "the OAuth2 
credential", true),
+
+  // Bearer
+  BEARER_TOKEN(String.class, OAuth2Properties.TOKEN, "the bearer token", true),
+
+  // SigV4
   AWS_ACCESS_KEY_ID(String.class, AwsProperties.REST_ACCESS_KEY_ID, "the aws 
access key id", true),
   AWS_SECRET_ACCESS_KEY(
-      String.class, AwsProperties.REST_SECRET_ACCESS_KEY, "the aws access key 
secret", true),
-  AWS_SESSION_TOKEN(
-      String.class, AwsProperties.REST_SESSION_TOKEN, "the aws scoped access 
token", true),
-  EXPIRATION_TIME(
+      String.class, AwsProperties.REST_SECRET_ACCESS_KEY, "the aws secret 
access key", true),
+  AWS_SESSION_TOKEN(String.class, AwsProperties.REST_SESSION_TOKEN, "the aws 
session token", true),
+  AWS_SESSION_TOKEN_EXPIRES_AT_MS(
+      Long.class,
+      "rest.session-token-expires-at-ms",
+      "the time the aws session token expires, in milliseconds",
+      false,
+      true),
+
+  // Metadata
+  EXPIRES_AT_MS(
       Long.class,
-      "expiration-time",
-      "the expiration time for the access token, in milliseconds",
-      false);
+      "rest.expires-at-ms",
+      "the expiration time for the access token or the credential, in 
milliseconds",
+      false,
+      true);
 
   private final Class valueType;
   private final String propertyName;
   private final String description;
   private final boolean isCredential;
+  private final boolean isExpirationTimestamp;
 
   CatalogAccessProperty(
       Class valueType, String propertyName, String description, boolean 
isCredential) {
+    this(valueType, propertyName, description, isCredential, false);
+  }
+
+  CatalogAccessProperty(
+      Class valueType,
+      String propertyName,
+      String description,
+      boolean isCredential,
+      boolean isExpirationTimestamp) {
     this.valueType = valueType;
     this.propertyName = propertyName;
     this.description = description;
     this.isCredential = isCredential;
+    this.isExpirationTimestamp = isExpirationTimestamp;
   }
 
   public String getPropertyName() {
@@ -61,6 +88,6 @@ public enum CatalogAccessProperty {
   }
 
   public boolean isExpirationTimestamp() {
-    return this == EXPIRATION_TIME;
+    return isExpirationTimestamp;
   }
 }
diff --git 
a/polaris-core/src/main/java/org/apache/polaris/core/credentials/connection/ConnectionCredentials.java
 
b/polaris-core/src/main/java/org/apache/polaris/core/credentials/connection/ConnectionCredentials.java
index d9e5e549c..bc8cd3e95 100644
--- 
a/polaris-core/src/main/java/org/apache/polaris/core/credentials/connection/ConnectionCredentials.java
+++ 
b/polaris-core/src/main/java/org/apache/polaris/core/credentials/connection/ConnectionCredentials.java
@@ -71,8 +71,10 @@ public interface ConnectionCredentials {
     default Builder put(CatalogAccessProperty key, String value) {
       if (key.isExpirationTimestamp()) {
         expiresAt(Instant.ofEpochMilli(Long.parseLong(value)));
-      } else if (key.isCredential()) {
-        putCredential(key.getPropertyName(), value);
+      }
+
+      if (key.isCredential()) {
+        return putCredential(key.getPropertyName(), value);
       }
       return this;
     }
diff --git 
a/runtime/service/src/main/java/org/apache/polaris/service/catalog/common/CatalogHandler.java
 
b/runtime/service/src/main/java/org/apache/polaris/service/catalog/common/CatalogHandler.java
index 6cfd16321..4be7a55f5 100644
--- 
a/runtime/service/src/main/java/org/apache/polaris/service/catalog/common/CatalogHandler.java
+++ 
b/runtime/service/src/main/java/org/apache/polaris/service/catalog/common/CatalogHandler.java
@@ -48,7 +48,6 @@ import 
org.apache.polaris.core.persistence.resolver.PolarisResolutionManifest;
 import org.apache.polaris.core.persistence.resolver.ResolutionManifestFactory;
 import org.apache.polaris.core.persistence.resolver.ResolverPath;
 import org.apache.polaris.core.persistence.resolver.ResolverStatus;
-import org.apache.polaris.core.secrets.UserSecretsManager;
 import org.apache.polaris.service.types.PolicyIdentifier;
 
 /**
@@ -64,7 +63,6 @@ public abstract class CatalogHandler {
   protected final ResolutionManifestFactory resolutionManifestFactory;
   protected final String catalogName;
   protected final PolarisAuthorizer authorizer;
-  protected final UserSecretsManager userSecretsManager;
   protected final PolarisCredentialManager credentialManager;
   protected final Instance<ExternalCatalogFactory> externalCatalogFactories;
 
@@ -81,7 +79,6 @@ public abstract class CatalogHandler {
       SecurityContext securityContext,
       String catalogName,
       PolarisAuthorizer authorizer,
-      UserSecretsManager userSecretsManager,
       PolarisCredentialManager credentialManager,
       Instance<ExternalCatalogFactory> externalCatalogFactories) {
     this.diagnostics = diagnostics;
@@ -98,15 +95,10 @@ public abstract class CatalogHandler {
     this.securityContext = securityContext;
     this.polarisPrincipal = (PolarisPrincipal) 
securityContext.getUserPrincipal();
     this.authorizer = authorizer;
-    this.userSecretsManager = userSecretsManager;
     this.credentialManager = credentialManager;
     this.externalCatalogFactories = externalCatalogFactories;
   }
 
-  protected UserSecretsManager getUserSecretsManager() {
-    return userSecretsManager;
-  }
-
   protected PolarisCredentialManager getPolarisCredentialManager() {
     return credentialManager;
   }
diff --git 
a/runtime/service/src/main/java/org/apache/polaris/service/catalog/generic/GenericTableCatalogAdapter.java
 
b/runtime/service/src/main/java/org/apache/polaris/service/catalog/generic/GenericTableCatalogAdapter.java
index ae16db5ba..5cf634aed 100644
--- 
a/runtime/service/src/main/java/org/apache/polaris/service/catalog/generic/GenericTableCatalogAdapter.java
+++ 
b/runtime/service/src/main/java/org/apache/polaris/service/catalog/generic/GenericTableCatalogAdapter.java
@@ -35,7 +35,6 @@ import org.apache.polaris.core.context.RealmContext;
 import org.apache.polaris.core.credentials.PolarisCredentialManager;
 import org.apache.polaris.core.persistence.PolarisMetaStoreManager;
 import org.apache.polaris.core.persistence.resolver.ResolutionManifestFactory;
-import org.apache.polaris.core.secrets.UserSecretsManager;
 import org.apache.polaris.service.catalog.CatalogPrefixParser;
 import 
org.apache.polaris.service.catalog.api.PolarisCatalogGenericTableApiService;
 import org.apache.polaris.service.catalog.common.CatalogAdapter;
@@ -61,7 +60,6 @@ public class GenericTableCatalogAdapter
   private final PolarisAuthorizer polarisAuthorizer;
   private final ReservedProperties reservedProperties;
   private final CatalogPrefixParser prefixParser;
-  private final UserSecretsManager userSecretsManager;
   private final PolarisCredentialManager polarisCredentialManager;
   private final Instance<ExternalCatalogFactory> externalCatalogFactories;
 
@@ -75,7 +73,6 @@ public class GenericTableCatalogAdapter
       PolarisAuthorizer polarisAuthorizer,
       CatalogPrefixParser prefixParser,
       ReservedProperties reservedProperties,
-      UserSecretsManager userSecretsManager,
       PolarisCredentialManager polarisCredentialManager,
       @Any Instance<ExternalCatalogFactory> externalCatalogFactories) {
     this.diagnostics = diagnostics;
@@ -87,7 +84,6 @@ public class GenericTableCatalogAdapter
     this.polarisAuthorizer = polarisAuthorizer;
     this.prefixParser = prefixParser;
     this.reservedProperties = reservedProperties;
-    this.userSecretsManager = userSecretsManager;
     this.polarisCredentialManager = polarisCredentialManager;
     this.externalCatalogFactories = externalCatalogFactories;
   }
@@ -106,7 +102,6 @@ public class GenericTableCatalogAdapter
         securityContext,
         prefixParser.prefixToCatalogName(realmContext, prefix),
         polarisAuthorizer,
-        userSecretsManager,
         polarisCredentialManager,
         externalCatalogFactories);
   }
diff --git 
a/runtime/service/src/main/java/org/apache/polaris/service/catalog/generic/GenericTableCatalogHandler.java
 
b/runtime/service/src/main/java/org/apache/polaris/service/catalog/generic/GenericTableCatalogHandler.java
index 4564e117f..55d52070f 100644
--- 
a/runtime/service/src/main/java/org/apache/polaris/service/catalog/generic/GenericTableCatalogHandler.java
+++ 
b/runtime/service/src/main/java/org/apache/polaris/service/catalog/generic/GenericTableCatalogHandler.java
@@ -40,7 +40,6 @@ import org.apache.polaris.core.entity.PolarisEntitySubType;
 import org.apache.polaris.core.entity.table.GenericTableEntity;
 import org.apache.polaris.core.persistence.PolarisMetaStoreManager;
 import org.apache.polaris.core.persistence.resolver.ResolutionManifestFactory;
-import org.apache.polaris.core.secrets.UserSecretsManager;
 import org.apache.polaris.service.catalog.common.CatalogHandler;
 import org.apache.polaris.service.types.GenericTable;
 import org.apache.polaris.service.types.ListGenericTablesResponse;
@@ -63,7 +62,6 @@ public class GenericTableCatalogHandler extends 
CatalogHandler {
       SecurityContext securityContext,
       String catalogName,
       PolarisAuthorizer authorizer,
-      UserSecretsManager userSecretsManager,
       PolarisCredentialManager polarisCredentialManager,
       Instance<ExternalCatalogFactory> externalCatalogFactories) {
     super(
@@ -73,7 +71,6 @@ public class GenericTableCatalogHandler extends 
CatalogHandler {
         securityContext,
         catalogName,
         authorizer,
-        userSecretsManager,
         polarisCredentialManager,
         externalCatalogFactories);
     this.metaStoreManager = metaStoreManager;
@@ -104,7 +101,7 @@ public class GenericTableCatalogHandler extends 
CatalogHandler {
         federatedCatalog =
             externalCatalogFactory
                 .get()
-                .createGenericCatalog(connectionConfigInfoDpo, 
getUserSecretsManager());
+                .createGenericCatalog(connectionConfigInfoDpo, 
getPolarisCredentialManager());
       } else {
         throw new UnsupportedOperationException(
             "External catalog factory for type '" + connectionType + "' is 
unavailable.");
diff --git 
a/runtime/service/src/main/java/org/apache/polaris/service/catalog/iceberg/IcebergCatalogAdapter.java
 
b/runtime/service/src/main/java/org/apache/polaris/service/catalog/iceberg/IcebergCatalogAdapter.java
index 0f622cb0b..90813a221 100644
--- 
a/runtime/service/src/main/java/org/apache/polaris/service/catalog/iceberg/IcebergCatalogAdapter.java
+++ 
b/runtime/service/src/main/java/org/apache/polaris/service/catalog/iceberg/IcebergCatalogAdapter.java
@@ -77,7 +77,6 @@ import 
org.apache.polaris.core.persistence.resolver.ResolverFactory;
 import org.apache.polaris.core.persistence.resolver.ResolverStatus;
 import org.apache.polaris.core.rest.PolarisEndpoints;
 import org.apache.polaris.core.rest.PolarisResourcePaths;
-import org.apache.polaris.core.secrets.UserSecretsManager;
 import org.apache.polaris.service.catalog.AccessDelegationMode;
 import org.apache.polaris.service.catalog.CatalogPrefixParser;
 import org.apache.polaris.service.catalog.api.IcebergRestCatalogApiService;
@@ -149,7 +148,6 @@ public class IcebergCatalogAdapter
   private final ResolutionManifestFactory resolutionManifestFactory;
   private final ResolverFactory resolverFactory;
   private final PolarisMetaStoreManager metaStoreManager;
-  private final UserSecretsManager userSecretsManager;
   private final PolarisCredentialManager credentialManager;
   private final PolarisAuthorizer polarisAuthorizer;
   private final CatalogPrefixParser prefixParser;
@@ -168,7 +166,6 @@ public class IcebergCatalogAdapter
       ResolverFactory resolverFactory,
       ResolutionManifestFactory resolutionManifestFactory,
       PolarisMetaStoreManager metaStoreManager,
-      UserSecretsManager userSecretsManager,
       PolarisCredentialManager credentialManager,
       PolarisAuthorizer polarisAuthorizer,
       CatalogPrefixParser prefixParser,
@@ -185,7 +182,6 @@ public class IcebergCatalogAdapter
     this.resolutionManifestFactory = resolutionManifestFactory;
     this.resolverFactory = resolverFactory;
     this.metaStoreManager = metaStoreManager;
-    this.userSecretsManager = userSecretsManager;
     this.credentialManager = credentialManager;
     this.polarisAuthorizer = polarisAuthorizer;
     this.prefixParser = prefixParser;
@@ -225,7 +221,6 @@ public class IcebergCatalogAdapter
         callContext,
         resolutionManifestFactory,
         metaStoreManager,
-        userSecretsManager,
         credentialManager,
         securityContext,
         catalogFactory,
diff --git 
a/runtime/service/src/main/java/org/apache/polaris/service/catalog/iceberg/IcebergCatalogHandler.java
 
b/runtime/service/src/main/java/org/apache/polaris/service/catalog/iceberg/IcebergCatalogHandler.java
index 07af147d7..c3b92971a 100644
--- 
a/runtime/service/src/main/java/org/apache/polaris/service/catalog/iceberg/IcebergCatalogHandler.java
+++ 
b/runtime/service/src/main/java/org/apache/polaris/service/catalog/iceberg/IcebergCatalogHandler.java
@@ -99,7 +99,6 @@ import 
org.apache.polaris.core.persistence.dao.entity.EntityWithPath;
 import org.apache.polaris.core.persistence.pagination.Page;
 import org.apache.polaris.core.persistence.pagination.PageToken;
 import org.apache.polaris.core.persistence.resolver.ResolutionManifestFactory;
-import org.apache.polaris.core.secrets.UserSecretsManager;
 import org.apache.polaris.core.storage.AccessConfig;
 import org.apache.polaris.core.storage.PolarisStorageActions;
 import org.apache.polaris.core.storage.StorageUtil;
@@ -156,7 +155,6 @@ public class IcebergCatalogHandler extends CatalogHandler 
implements AutoCloseab
       CallContext callContext,
       ResolutionManifestFactory resolutionManifestFactory,
       PolarisMetaStoreManager metaStoreManager,
-      UserSecretsManager userSecretsManager,
       PolarisCredentialManager credentialManager,
       SecurityContext securityContext,
       CallContextCatalogFactory catalogFactory,
@@ -174,7 +172,6 @@ public class IcebergCatalogHandler extends CatalogHandler 
implements AutoCloseab
         securityContext,
         catalogName,
         authorizer,
-        userSecretsManager,
         credentialManager,
         externalCatalogFactories);
     this.metaStoreManager = metaStoreManager;
@@ -258,10 +255,7 @@ public class IcebergCatalogHandler extends CatalogHandler 
implements AutoCloseab
         federatedCatalog =
             externalCatalogFactory
                 .get()
-                .createCatalog(
-                    connectionConfigInfoDpo,
-                    getUserSecretsManager(),
-                    getPolarisCredentialManager());
+                .createCatalog(connectionConfigInfoDpo, 
getPolarisCredentialManager());
       } else {
         throw new UnsupportedOperationException(
             "External catalog factory for type '" + connectionType + "' is 
unavailable.");
diff --git 
a/runtime/service/src/main/java/org/apache/polaris/service/catalog/iceberg/IcebergRESTExternalCatalogFactory.java
 
b/runtime/service/src/main/java/org/apache/polaris/service/catalog/iceberg/IcebergRESTExternalCatalogFactory.java
index 7167e382e..de12bed4c 100644
--- 
a/runtime/service/src/main/java/org/apache/polaris/service/catalog/iceberg/IcebergRESTExternalCatalogFactory.java
+++ 
b/runtime/service/src/main/java/org/apache/polaris/service/catalog/iceberg/IcebergRESTExternalCatalogFactory.java
@@ -30,7 +30,6 @@ import 
org.apache.polaris.core.connection.ConnectionConfigInfoDpo;
 import org.apache.polaris.core.connection.ConnectionType;
 import 
org.apache.polaris.core.connection.iceberg.IcebergRestConnectionConfigInfoDpo;
 import org.apache.polaris.core.credentials.PolarisCredentialManager;
-import org.apache.polaris.core.secrets.UserSecretsManager;
 
 /** Factory class for creating an Iceberg REST catalog handle based on 
connection configuration. */
 @ApplicationScoped
@@ -39,9 +38,7 @@ public class IcebergRESTExternalCatalogFactory implements 
ExternalCatalogFactory
 
   @Override
   public Catalog createCatalog(
-      ConnectionConfigInfoDpo connectionConfig,
-      UserSecretsManager userSecretsManager,
-      PolarisCredentialManager polarisCredentialManager) {
+      ConnectionConfigInfoDpo connectionConfig, PolarisCredentialManager 
polarisCredentialManager) {
     if (!(connectionConfig instanceof IcebergRestConnectionConfigInfoDpo 
icebergConfig)) {
       throw new IllegalArgumentException(
           "Expected IcebergRestConnectionConfigInfoDpo but got: "
@@ -59,14 +56,14 @@ public class IcebergRESTExternalCatalogFactory implements 
ExternalCatalogFactory
 
     federatedCatalog.initialize(
         icebergConfig.getRemoteCatalogName(),
-        connectionConfig.asIcebergCatalogProperties(userSecretsManager, 
polarisCredentialManager));
+        connectionConfig.asIcebergCatalogProperties(polarisCredentialManager));
 
     return federatedCatalog;
   }
 
   @Override
   public GenericTableCatalog createGenericCatalog(
-      ConnectionConfigInfoDpo connectionConfig, UserSecretsManager 
userSecretsManager) {
+      ConnectionConfigInfoDpo connectionConfig, PolarisCredentialManager 
polarisCredentialManager) {
     // TODO implement
     throw new UnsupportedOperationException(
         "Generic table federation to this catalog is not supported.");
diff --git 
a/runtime/service/src/main/java/org/apache/polaris/service/catalog/policy/PolicyCatalogAdapter.java
 
b/runtime/service/src/main/java/org/apache/polaris/service/catalog/policy/PolicyCatalogAdapter.java
index 545848065..aa5e75fee 100644
--- 
a/runtime/service/src/main/java/org/apache/polaris/service/catalog/policy/PolicyCatalogAdapter.java
+++ 
b/runtime/service/src/main/java/org/apache/polaris/service/catalog/policy/PolicyCatalogAdapter.java
@@ -37,7 +37,6 @@ import 
org.apache.polaris.core.credentials.PolarisCredentialManager;
 import org.apache.polaris.core.persistence.PolarisMetaStoreManager;
 import org.apache.polaris.core.persistence.resolver.ResolutionManifestFactory;
 import org.apache.polaris.core.policy.PolicyType;
-import org.apache.polaris.core.secrets.UserSecretsManager;
 import org.apache.polaris.service.catalog.CatalogPrefixParser;
 import org.apache.polaris.service.catalog.api.PolarisCatalogPolicyApiService;
 import org.apache.polaris.service.catalog.common.CatalogAdapter;
@@ -64,7 +63,6 @@ public class PolicyCatalogAdapter implements 
PolarisCatalogPolicyApiService, Cat
   private final PolarisMetaStoreManager metaStoreManager;
   private final PolarisAuthorizer polarisAuthorizer;
   private final CatalogPrefixParser prefixParser;
-  private final UserSecretsManager userSecretsManager;
   private final PolarisCredentialManager polarisCredentialManager;
   private final Instance<ExternalCatalogFactory> externalCatalogFactories;
 
@@ -77,7 +75,6 @@ public class PolicyCatalogAdapter implements 
PolarisCatalogPolicyApiService, Cat
       PolarisMetaStoreManager metaStoreManager,
       PolarisAuthorizer polarisAuthorizer,
       CatalogPrefixParser prefixParser,
-      UserSecretsManager userSecretsManager,
       PolarisCredentialManager polarisCredentialManager,
       @Any Instance<ExternalCatalogFactory> externalCatalogFactories) {
     this.diagnostics = diagnostics;
@@ -88,7 +85,6 @@ public class PolicyCatalogAdapter implements 
PolarisCatalogPolicyApiService, Cat
     this.metaStoreManager = metaStoreManager;
     this.polarisAuthorizer = polarisAuthorizer;
     this.prefixParser = prefixParser;
-    this.userSecretsManager = userSecretsManager;
     this.polarisCredentialManager = polarisCredentialManager;
     this.externalCatalogFactories = externalCatalogFactories;
   }
@@ -106,7 +102,6 @@ public class PolicyCatalogAdapter implements 
PolarisCatalogPolicyApiService, Cat
         securityContext,
         prefixParser.prefixToCatalogName(realmContext, prefix),
         polarisAuthorizer,
-        userSecretsManager,
         polarisCredentialManager,
         externalCatalogFactories);
   }
diff --git 
a/runtime/service/src/main/java/org/apache/polaris/service/catalog/policy/PolicyCatalogHandler.java
 
b/runtime/service/src/main/java/org/apache/polaris/service/catalog/policy/PolicyCatalogHandler.java
index 0c92c816f..504b0a8d3 100644
--- 
a/runtime/service/src/main/java/org/apache/polaris/service/catalog/policy/PolicyCatalogHandler.java
+++ 
b/runtime/service/src/main/java/org/apache/polaris/service/catalog/policy/PolicyCatalogHandler.java
@@ -46,7 +46,6 @@ import 
org.apache.polaris.core.persistence.resolver.ResolverPath;
 import org.apache.polaris.core.persistence.resolver.ResolverStatus;
 import org.apache.polaris.core.policy.PolicyType;
 import org.apache.polaris.core.policy.exceptions.NoSuchPolicyException;
-import org.apache.polaris.core.secrets.UserSecretsManager;
 import org.apache.polaris.service.catalog.common.CatalogHandler;
 import org.apache.polaris.service.types.AttachPolicyRequest;
 import org.apache.polaris.service.types.CreatePolicyRequest;
@@ -72,7 +71,6 @@ public class PolicyCatalogHandler extends CatalogHandler {
       SecurityContext securityContext,
       String catalogName,
       PolarisAuthorizer authorizer,
-      UserSecretsManager userSecretsManager,
       PolarisCredentialManager polarisCredentialManager,
       Instance<ExternalCatalogFactory> externalCatalogFactories) {
     super(
@@ -82,7 +80,6 @@ public class PolicyCatalogHandler extends CatalogHandler {
         securityContext,
         catalogName,
         authorizer,
-        userSecretsManager,
         polarisCredentialManager,
         externalCatalogFactories);
     this.metaStoreManager = metaStoreManager;
diff --git 
a/runtime/service/src/main/java/org/apache/polaris/service/config/ProductionReadinessChecks.java
 
b/runtime/service/src/main/java/org/apache/polaris/service/config/ProductionReadinessChecks.java
index 1c9cedd4c..51a1e1008 100644
--- 
a/runtime/service/src/main/java/org/apache/polaris/service/config/ProductionReadinessChecks.java
+++ 
b/runtime/service/src/main/java/org/apache/polaris/service/config/ProductionReadinessChecks.java
@@ -33,6 +33,7 @@ import java.util.List;
 import org.apache.polaris.core.config.FeatureConfiguration;
 import org.apache.polaris.core.config.ProductionReadinessCheck;
 import org.apache.polaris.core.config.ProductionReadinessCheck.Error;
+import 
org.apache.polaris.core.credentials.connection.ConnectionCredentialVendor;
 import org.apache.polaris.core.persistence.MetaStoreManagerFactory;
 import org.apache.polaris.service.auth.AuthenticationConfiguration;
 import 
org.apache.polaris.service.auth.AuthenticationRealmConfiguration.TokenBrokerConfiguration.RSAKeyPairConfiguration;
@@ -42,6 +43,7 @@ import 
org.apache.polaris.service.catalog.validation.IcebergPropertiesValidation
 import org.apache.polaris.service.context.DefaultRealmContextResolver;
 import org.apache.polaris.service.context.RealmContextResolver;
 import org.apache.polaris.service.context.TestRealmContextResolver;
+import org.apache.polaris.service.credentials.connection.AuthType;
 import org.apache.polaris.service.events.listeners.PolarisEventListener;
 import org.apache.polaris.service.events.listeners.TestPolarisEventListener;
 import org.apache.polaris.service.metrics.MetricsConfiguration;
@@ -335,4 +337,64 @@ public class ProductionReadinessChecks {
         ? ProductionReadinessCheck.OK
         : ProductionReadinessCheck.of(errors.toArray(new Error[0]));
   }
+
+  @Produces
+  @SuppressWarnings("unchecked")
+  public ProductionReadinessCheck checkConnectionCredentialVendors(
+      Instance<ConnectionCredentialVendor> credentialVendors,
+      FeaturesConfiguration featureConfiguration) {
+    var mapper = new ObjectMapper();
+    var defaults = featureConfiguration.parseDefaults(mapper);
+    var realmOverrides = featureConfiguration.parseRealmOverrides(mapper);
+
+    var federationKey = FeatureConfiguration.ENABLE_CATALOG_FEDERATION.key();
+    var authTypesKey = 
FeatureConfiguration.SUPPORTED_EXTERNAL_CATALOG_AUTHENTICATION_TYPES.key();
+    var federationEnabled =
+        Boolean.parseBoolean(defaults.getOrDefault(federationKey, 
false).toString());
+    var defaultAuthTypes = (List<String>) defaults.getOrDefault(authTypesKey, 
List.of());
+
+    var allAuthTypes = new java.util.HashSet<String>();
+    if (federationEnabled) allAuthTypes.addAll(defaultAuthTypes);
+
+    realmOverrides.forEach(
+        (id, overrides) -> {
+          if (Boolean.parseBoolean(
+              overrides.getOrDefault(federationKey, 
federationEnabled).toString())) {
+            allAuthTypes.addAll(
+                (List<String>)
+                    (overrides.containsKey(authTypesKey)
+                        ? overrides.get(authTypesKey)
+                        : defaultAuthTypes));
+          }
+        });
+
+    var errors = new ArrayList<Error>();
+    for (var name : allAuthTypes) {
+      try {
+        var type = 
org.apache.polaris.core.connection.AuthenticationType.valueOf(name);
+        if 
(credentialVendors.select(AuthType.Literal.of(type)).isUnsatisfied()) {
+          errors.add(
+              Error.ofSevere(
+                  format(
+                      "Catalog federation is enabled but no 
ConnectionCredentialVendor found for "
+                          + "authentication type '%s'. External catalog 
connections using this "
+                          + "authentication type will fail.",
+                      type),
+                  format("polaris.features.\"%s\"", authTypesKey)));
+        }
+      } catch (IllegalArgumentException e) {
+        errors.add(
+            Error.ofSevere(
+                format(
+                    "Invalid authentication type '%s' in 
SUPPORTED_EXTERNAL_CATALOG_AUTHENTICATION_TYPES "
+                        + "configuration.",
+                    name),
+                format("polaris.features.\"%s\"", authTypesKey)));
+      }
+    }
+
+    return errors.isEmpty()
+        ? ProductionReadinessCheck.OK
+        : ProductionReadinessCheck.of(errors.toArray(new Error[0]));
+  }
 }
diff --git 
a/polaris-core/src/main/java/org/apache/polaris/core/connection/iceberg/IcebergCatalogPropertiesProvider.java
 
b/runtime/service/src/main/java/org/apache/polaris/service/credentials/CredentialVendorPriorities.java
similarity index 50%
copy from 
polaris-core/src/main/java/org/apache/polaris/core/connection/iceberg/IcebergCatalogPropertiesProvider.java
copy to 
runtime/service/src/main/java/org/apache/polaris/service/credentials/CredentialVendorPriorities.java
index e17218f25..2325a90a4 100644
--- 
a/polaris-core/src/main/java/org/apache/polaris/core/connection/iceberg/IcebergCatalogPropertiesProvider.java
+++ 
b/runtime/service/src/main/java/org/apache/polaris/service/credentials/CredentialVendorPriorities.java
@@ -16,21 +16,21 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.polaris.core.connection.iceberg;
+package org.apache.polaris.service.credentials;
 
-import jakarta.annotation.Nonnull;
-import java.util.Map;
-import org.apache.polaris.core.credentials.PolarisCredentialManager;
-import org.apache.polaris.core.secrets.UserSecretsManager;
+import 
org.apache.polaris.core.credentials.connection.ConnectionCredentialVendor;
 
 /**
- * Configuration wrappers which ultimately translate their contents into 
Iceberg properties and
- * which may hold other nested configuration wrapper objects implement this 
interface to allow
- * delegating type-specific configuration translation logic to subclasses 
instead of needing to
- * expose the internals of deeply nested configuration objects to a visitor 
class.
+ * Priority constants for credential vendor implementations. e.g., {@link
+ * ConnectionCredentialVendor}
+ *
+ * <p>Higher priority values are selected first when multiple vendors support 
the same
+ * authentication or storage type. Default built-in implementations use {@code 
DEFAULT}. Custom
+ * implementations should use a higher priority value to override defaults.
  */
-public interface IcebergCatalogPropertiesProvider {
-  @Nonnull
-  Map<String, String> asIcebergCatalogProperties(
-      UserSecretsManager secretsManager, PolarisCredentialManager 
credentialManager);
+public final class CredentialVendorPriorities {
+  /** Priority for default built-in vendor implementations. */
+  public static final int DEFAULT = 100;
+
+  private CredentialVendorPriorities() {}
 }
diff --git 
a/runtime/service/src/main/java/org/apache/polaris/service/credentials/connection/AuthType.java
 
b/runtime/service/src/main/java/org/apache/polaris/service/credentials/connection/AuthType.java
index ada9825bf..832858463 100644
--- 
a/runtime/service/src/main/java/org/apache/polaris/service/credentials/connection/AuthType.java
+++ 
b/runtime/service/src/main/java/org/apache/polaris/service/credentials/connection/AuthType.java
@@ -39,7 +39,7 @@ import 
org.apache.polaris.core.credentials.connection.ConnectionCredentialVendor
  * <pre>{@code
  * @ApplicationScoped
  * @AuthType(AuthenticationType.SIGV4)
- * @Priority(100)
+ * @Priority(CredentialVendorPriorities.DEFAULT)
  * public class SigV4ConnectionCredentialVendor implements 
ConnectionCredentialVendor {
  *   // AWS STS AssumeRole logic for SigV4 authentication
  * }
diff --git 
a/runtime/service/src/main/java/org/apache/polaris/service/credentials/connection/BearerConnectionCredentialVendor.java
 
b/runtime/service/src/main/java/org/apache/polaris/service/credentials/connection/BearerConnectionCredentialVendor.java
new file mode 100644
index 000000000..75e1551f7
--- /dev/null
+++ 
b/runtime/service/src/main/java/org/apache/polaris/service/credentials/connection/BearerConnectionCredentialVendor.java
@@ -0,0 +1,85 @@
+/*
+ * 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.polaris.service.credentials.connection;
+
+import com.google.common.base.Preconditions;
+import jakarta.annotation.Nonnull;
+import jakarta.annotation.Priority;
+import jakarta.enterprise.context.RequestScoped;
+import jakarta.inject.Inject;
+import org.apache.polaris.core.connection.AuthenticationType;
+import org.apache.polaris.core.connection.BearerAuthenticationParametersDpo;
+import org.apache.polaris.core.connection.ConnectionConfigInfoDpo;
+import org.apache.polaris.core.credentials.connection.CatalogAccessProperty;
+import 
org.apache.polaris.core.credentials.connection.ConnectionCredentialVendor;
+import org.apache.polaris.core.credentials.connection.ConnectionCredentials;
+import org.apache.polaris.core.secrets.UserSecretsManager;
+import org.apache.polaris.service.credentials.CredentialVendorPriorities;
+
+/**
+ * Connection credential vendor for Bearer token authentication.
+ *
+ * <p>This vendor handles Bearer token authentication by reading the bearer 
token from the secrets
+ * manager and providing it for connecting to external catalogs.
+ *
+ * <p>The vendor provides only the bearer token. When connecting to the remote 
catalog, Iceberg SDK
+ * will use this token as-is in HTTP Authorization headers. Bearer tokens 
typically have a limited
+ * lifetime and should be refreshed by the user when they expire.
+ *
+ * <p>This is the default implementation with {@code 
@Priority(CredentialVendorPriorities.DEFAULT)}.
+ * Custom implementations can override this by providing a higher priority 
value.
+ */
+@RequestScoped
+@AuthType(AuthenticationType.BEARER)
+@Priority(CredentialVendorPriorities.DEFAULT)
+public class BearerConnectionCredentialVendor implements 
ConnectionCredentialVendor {
+
+  private final UserSecretsManager secretsManager;
+
+  @Inject
+  public BearerConnectionCredentialVendor(UserSecretsManager secretsManager) {
+    this.secretsManager = secretsManager;
+  }
+
+  @Override
+  public @Nonnull ConnectionCredentials getConnectionCredentials(
+      @Nonnull ConnectionConfigInfoDpo connectionConfig) {
+
+    // Validate authentication parameters type
+    Preconditions.checkArgument(
+        connectionConfig.getAuthenticationParameters() instanceof 
BearerAuthenticationParametersDpo,
+        "Expected BearerAuthenticationParametersDpo, got: %s",
+        connectionConfig.getAuthenticationParameters().getClass().getName());
+
+    BearerAuthenticationParametersDpo bearerParams =
+        (BearerAuthenticationParametersDpo) 
connectionConfig.getAuthenticationParameters();
+
+    // Read the bearer token from secrets manager
+    String bearerToken = 
secretsManager.readSecret(bearerParams.getBearerTokenReference());
+
+    // Return the bearer token with expiration
+    // Bearer tokens don't expire from Polaris's perspective - set expiration 
to Long.MAX_VALUE
+    // to indicate infinite validity. The token itself may have an expiration, 
but that's managed
+    // by the token issuer, not Polaris.
+    return ConnectionCredentials.builder()
+        .put(CatalogAccessProperty.BEARER_TOKEN, bearerToken)
+        .put(CatalogAccessProperty.EXPIRES_AT_MS, 
String.valueOf(Long.MAX_VALUE))
+        .build();
+  }
+}
diff --git 
a/runtime/service/src/main/java/org/apache/polaris/service/credentials/connection/ImplicitConnectionCredentialVendor.java
 
b/runtime/service/src/main/java/org/apache/polaris/service/credentials/connection/ImplicitConnectionCredentialVendor.java
new file mode 100644
index 000000000..ecef5f5bf
--- /dev/null
+++ 
b/runtime/service/src/main/java/org/apache/polaris/service/credentials/connection/ImplicitConnectionCredentialVendor.java
@@ -0,0 +1,68 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.polaris.service.credentials.connection;
+
+import com.google.common.base.Preconditions;
+import jakarta.annotation.Nonnull;
+import jakarta.annotation.Priority;
+import jakarta.enterprise.context.RequestScoped;
+import org.apache.polaris.core.connection.AuthenticationType;
+import org.apache.polaris.core.connection.ConnectionConfigInfoDpo;
+import org.apache.polaris.core.connection.ImplicitAuthenticationParametersDpo;
+import org.apache.polaris.core.credentials.connection.CatalogAccessProperty;
+import 
org.apache.polaris.core.credentials.connection.ConnectionCredentialVendor;
+import org.apache.polaris.core.credentials.connection.ConnectionCredentials;
+import org.apache.polaris.service.credentials.CredentialVendorPriorities;
+
+/**
+ * Connection credential vendor for Implicit (no authentication) type.
+ *
+ * <p>This vendor handles implicit authentication where no credentials are 
required to connect to
+ * external catalogs.
+ *
+ * <p>The vendor provides no credentials. When connecting to the remote 
catalog, Iceberg SDK will
+ * not send any authentication headers or credentials. This is typically used 
for publicly
+ * accessible catalogs or when authentication is handled externally.
+ *
+ * <p>This is the default implementation with {@code 
@Priority(CredentialVendorPriorities.DEFAULT)}.
+ * Custom implementations can override this by providing a higher priority 
value.
+ */
+@RequestScoped
+@AuthType(AuthenticationType.IMPLICIT)
+@Priority(CredentialVendorPriorities.DEFAULT)
+public class ImplicitConnectionCredentialVendor implements 
ConnectionCredentialVendor {
+
+  @Override
+  public @Nonnull ConnectionCredentials getConnectionCredentials(
+      @Nonnull ConnectionConfigInfoDpo connectionConfig) {
+
+    // Validate authentication parameters type
+    Preconditions.checkArgument(
+        connectionConfig.getAuthenticationParameters()
+            instanceof ImplicitAuthenticationParametersDpo,
+        "Expected ImplicitAuthenticationParametersDpo, got: %s",
+        connectionConfig.getAuthenticationParameters().getClass().getName());
+
+    // Return empty credentials for implicit (no authentication) type with 
expiration
+    // Set expiration to Long.MAX_VALUE to indicate infinite validity
+    return ConnectionCredentials.builder()
+        .put(CatalogAccessProperty.EXPIRES_AT_MS, 
String.valueOf(Long.MAX_VALUE))
+        .build();
+  }
+}
diff --git 
a/runtime/service/src/main/java/org/apache/polaris/service/credentials/connection/OAuthClientCredentialVendor.java
 
b/runtime/service/src/main/java/org/apache/polaris/service/credentials/connection/OAuthClientCredentialVendor.java
new file mode 100644
index 000000000..993613259
--- /dev/null
+++ 
b/runtime/service/src/main/java/org/apache/polaris/service/credentials/connection/OAuthClientCredentialVendor.java
@@ -0,0 +1,92 @@
+/*
+ * 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.polaris.service.credentials.connection;
+
+import com.google.common.base.Joiner;
+import com.google.common.base.Preconditions;
+import jakarta.annotation.Nonnull;
+import jakarta.annotation.Priority;
+import jakarta.enterprise.context.RequestScoped;
+import jakarta.inject.Inject;
+import org.apache.polaris.core.connection.AuthenticationType;
+import org.apache.polaris.core.connection.ConnectionConfigInfoDpo;
+import org.apache.polaris.core.connection.OAuthClientCredentialsParametersDpo;
+import org.apache.polaris.core.credentials.connection.CatalogAccessProperty;
+import 
org.apache.polaris.core.credentials.connection.ConnectionCredentialVendor;
+import org.apache.polaris.core.credentials.connection.ConnectionCredentials;
+import org.apache.polaris.core.secrets.UserSecretsManager;
+import org.apache.polaris.service.credentials.CredentialVendorPriorities;
+
+/**
+ * Connection credential vendor for OAuth 2.0 Client Credentials 
authentication.
+ *
+ * <p>This vendor handles OAuth 2.0 client credentials flow by reading the 
client secret from the
+ * secrets manager and formatting it as an OAuth credential for connecting to 
external catalogs.
+ *
+ * <p>The vendor provides only the OAuth credential (formatted as 
"clientId:clientSecret"). When
+ * connecting to the remote catalog, Iceberg SDK will use this credential to 
fetch OAuth tokens
+ * automatically.
+ *
+ * <p>This is the default implementation with {@code 
@Priority(CredentialVendorPriorities.DEFAULT)}.
+ * Custom implementations can override this by providing a higher priority 
value.
+ */
+@RequestScoped
+@AuthType(AuthenticationType.OAUTH)
+@Priority(CredentialVendorPriorities.DEFAULT)
+public class OAuthClientCredentialVendor implements ConnectionCredentialVendor 
{
+
+  private static final Joiner COLON_JOINER = Joiner.on(":");
+
+  private final UserSecretsManager secretsManager;
+
+  @Inject
+  public OAuthClientCredentialVendor(UserSecretsManager secretsManager) {
+    this.secretsManager = secretsManager;
+  }
+
+  @Override
+  public @Nonnull ConnectionCredentials getConnectionCredentials(
+      @Nonnull ConnectionConfigInfoDpo connectionConfig) {
+
+    // Validate authentication parameters type
+    Preconditions.checkArgument(
+        connectionConfig.getAuthenticationParameters()
+            instanceof OAuthClientCredentialsParametersDpo,
+        "Expected OAuthClientCredentialsParametersDpo, got: %s",
+        connectionConfig.getAuthenticationParameters().getClass().getName());
+
+    OAuthClientCredentialsParametersDpo oauthParams =
+        (OAuthClientCredentialsParametersDpo) 
connectionConfig.getAuthenticationParameters();
+
+    // Read the client secret from secrets manager
+    String clientSecret = 
secretsManager.readSecret(oauthParams.getClientSecretReference());
+
+    // Format credential as "clientId:clientSecret"
+    String credential = COLON_JOINER.join(oauthParams.getClientId(), 
clientSecret);
+
+    // Return the OAuth credential with expiration
+    // OAuth credentials don't expire from Polaris's perspective - set 
expiration to Long.MAX_VALUE
+    // to indicate infinite validity. If the credential expires, users need to 
update the catalog
+    // entity to rotate the credential.
+    return ConnectionCredentials.builder()
+        .put(CatalogAccessProperty.OAUTH2_CREDENTIAL, credential)
+        .put(CatalogAccessProperty.EXPIRES_AT_MS, 
String.valueOf(Long.MAX_VALUE))
+        .build();
+  }
+}
diff --git 
a/runtime/service/src/main/java/org/apache/polaris/service/credentials/connection/SigV4ConnectionCredentialVendor.java
 
b/runtime/service/src/main/java/org/apache/polaris/service/credentials/connection/SigV4ConnectionCredentialVendor.java
index d85d318cf..e0e230e4b 100644
--- 
a/runtime/service/src/main/java/org/apache/polaris/service/credentials/connection/SigV4ConnectionCredentialVendor.java
+++ 
b/runtime/service/src/main/java/org/apache/polaris/service/credentials/connection/SigV4ConnectionCredentialVendor.java
@@ -22,7 +22,7 @@ import com.google.common.annotations.VisibleForTesting;
 import com.google.common.base.Preconditions;
 import jakarta.annotation.Nonnull;
 import jakarta.annotation.Priority;
-import jakarta.enterprise.context.ApplicationScoped;
+import jakarta.enterprise.context.RequestScoped;
 import jakarta.inject.Inject;
 import java.util.Optional;
 import org.apache.polaris.core.connection.AuthenticationType;
@@ -35,6 +35,7 @@ import 
org.apache.polaris.core.identity.credential.AwsIamServiceIdentityCredenti
 import org.apache.polaris.core.identity.credential.ServiceIdentityCredential;
 import org.apache.polaris.core.identity.provider.ServiceIdentityProvider;
 import org.apache.polaris.core.storage.aws.StsClientProvider;
+import org.apache.polaris.service.credentials.CredentialVendorPriorities;
 import software.amazon.awssdk.services.sts.StsClient;
 import software.amazon.awssdk.services.sts.model.AssumeRoleRequest;
 import software.amazon.awssdk.services.sts.model.AssumeRoleResponse;
@@ -55,12 +56,12 @@ import 
software.amazon.awssdk.services.sts.model.AssumeRoleResponse;
  *   <li>Returns temporary access key, secret key, and session token
  * </ol>
  *
- * <p>This is the default implementation with {@code @Priority(100)}. Custom 
implementations can
- * override this by providing a higher priority value.
+ * <p>This is the default implementation with {@code 
@Priority(CredentialVendorPriorities.DEFAULT)}.
+ * Custom implementations can override this by providing a higher priority 
value.
  */
-@ApplicationScoped
+@RequestScoped
 @AuthType(AuthenticationType.SIGV4)
-@Priority(100)
+@Priority(CredentialVendorPriorities.DEFAULT)
 public class SigV4ConnectionCredentialVendor implements 
ConnectionCredentialVendor {
 
   private static final String DEFAULT_ROLE_SESSION_NAME = "polaris";
@@ -124,17 +125,16 @@ public class SigV4ConnectionCredentialVendor implements 
ConnectionCredentialVend
 
     // Build connection credentials from AWS temporary credentials
     ConnectionCredentials.Builder builder = ConnectionCredentials.builder();
-    builder.putCredential(
-        CatalogAccessProperty.AWS_ACCESS_KEY_ID.getPropertyName(),
-        response.credentials().accessKeyId());
-    builder.putCredential(
-        CatalogAccessProperty.AWS_SECRET_ACCESS_KEY.getPropertyName(),
-        response.credentials().secretAccessKey());
-    builder.putCredential(
-        CatalogAccessProperty.AWS_SESSION_TOKEN.getPropertyName(),
-        response.credentials().sessionToken());
+    builder.put(CatalogAccessProperty.AWS_ACCESS_KEY_ID, 
response.credentials().accessKeyId());
+    builder.put(
+        CatalogAccessProperty.AWS_SECRET_ACCESS_KEY, 
response.credentials().secretAccessKey());
+    builder.put(CatalogAccessProperty.AWS_SESSION_TOKEN, 
response.credentials().sessionToken());
     Optional.ofNullable(response.credentials().expiration())
-        .ifPresent(expiration -> builder.expiresAt(expiration));
+        .ifPresent(
+            expiration ->
+                builder.put(
+                    CatalogAccessProperty.AWS_SESSION_TOKEN_EXPIRES_AT_MS,
+                    String.valueOf(expiration.toEpochMilli())));
 
     return builder.build();
   }
diff --git 
a/runtime/service/src/test/java/org/apache/polaris/service/catalog/generic/PolarisGenericTableCatalogHandlerAuthzTest.java
 
b/runtime/service/src/test/java/org/apache/polaris/service/catalog/generic/PolarisGenericTableCatalogHandlerAuthzTest.java
index 00c87f899..b1296177b 100644
--- 
a/runtime/service/src/test/java/org/apache/polaris/service/catalog/generic/PolarisGenericTableCatalogHandlerAuthzTest.java
+++ 
b/runtime/service/src/test/java/org/apache/polaris/service/catalog/generic/PolarisGenericTableCatalogHandlerAuthzTest.java
@@ -54,7 +54,6 @@ public class PolarisGenericTableCatalogHandlerAuthzTest 
extends PolarisAuthzTest
         catalogName,
         polarisAuthorizer,
         null,
-        null,
         null);
   }
 
diff --git 
a/runtime/service/src/test/java/org/apache/polaris/service/catalog/iceberg/IcebergCatalogHandlerAuthzTest.java
 
b/runtime/service/src/test/java/org/apache/polaris/service/catalog/iceberg/IcebergCatalogHandlerAuthzTest.java
index 5148bf0ef..568c83c7d 100644
--- 
a/runtime/service/src/test/java/org/apache/polaris/service/catalog/iceberg/IcebergCatalogHandlerAuthzTest.java
+++ 
b/runtime/service/src/test/java/org/apache/polaris/service/catalog/iceberg/IcebergCatalogHandlerAuthzTest.java
@@ -126,7 +126,6 @@ public class IcebergCatalogHandlerAuthzTest extends 
PolarisAuthzTestBase {
         callContext,
         resolutionManifestFactory,
         metaStoreManager,
-        userSecretsManager,
         credentialManager,
         securityContext(authenticatedPrincipal),
         factory,
@@ -267,7 +266,6 @@ public class IcebergCatalogHandlerAuthzTest extends 
PolarisAuthzTestBase {
             callContext,
             resolutionManifestFactory,
             metaStoreManager,
-            userSecretsManager,
             credentialManager,
             securityContext(authenticatedPrincipal),
             callContextCatalogFactory,
@@ -306,7 +304,6 @@ public class IcebergCatalogHandlerAuthzTest extends 
PolarisAuthzTestBase {
             callContext,
             resolutionManifestFactory,
             metaStoreManager,
-            userSecretsManager,
             credentialManager,
             securityContext(authenticatedPrincipal1),
             callContextCatalogFactory,
@@ -1187,7 +1184,6 @@ public class IcebergCatalogHandlerAuthzTest extends 
PolarisAuthzTestBase {
         mockCallContext,
         resolutionManifestFactory,
         metaStoreManager,
-        userSecretsManager,
         credentialManager,
         securityContext(authenticatedPrincipal),
         factory,
diff --git 
a/runtime/service/src/test/java/org/apache/polaris/service/catalog/iceberg/IcebergCatalogHandlerFineGrainedDisabledTest.java
 
b/runtime/service/src/test/java/org/apache/polaris/service/catalog/iceberg/IcebergCatalogHandlerFineGrainedDisabledTest.java
index 367f9e6d7..e6434020e 100644
--- 
a/runtime/service/src/test/java/org/apache/polaris/service/catalog/iceberg/IcebergCatalogHandlerFineGrainedDisabledTest.java
+++ 
b/runtime/service/src/test/java/org/apache/polaris/service/catalog/iceberg/IcebergCatalogHandlerFineGrainedDisabledTest.java
@@ -62,7 +62,6 @@ public class IcebergCatalogHandlerFineGrainedDisabledTest 
extends PolarisAuthzTe
         callContext,
         resolutionManifestFactory,
         metaStoreManager,
-        userSecretsManager,
         credentialManager,
         securityContext(authenticatedPrincipal),
         callContextCatalogFactory,
diff --git 
a/runtime/service/src/test/java/org/apache/polaris/service/catalog/policy/PolicyCatalogHandlerAuthzTest.java
 
b/runtime/service/src/test/java/org/apache/polaris/service/catalog/policy/PolicyCatalogHandlerAuthzTest.java
index 9a85496db..990a9cff2 100644
--- 
a/runtime/service/src/test/java/org/apache/polaris/service/catalog/policy/PolicyCatalogHandlerAuthzTest.java
+++ 
b/runtime/service/src/test/java/org/apache/polaris/service/catalog/policy/PolicyCatalogHandlerAuthzTest.java
@@ -59,7 +59,6 @@ public class PolicyCatalogHandlerAuthzTest extends 
PolarisAuthzTestBase {
         catalogName,
         polarisAuthorizer,
         null,
-        null,
         null);
   }
 
diff --git 
a/runtime/service/src/test/java/org/apache/polaris/service/credentials/connection/BearerConnectionCredentialVendorTest.java
 
b/runtime/service/src/test/java/org/apache/polaris/service/credentials/connection/BearerConnectionCredentialVendorTest.java
new file mode 100644
index 000000000..d8c014142
--- /dev/null
+++ 
b/runtime/service/src/test/java/org/apache/polaris/service/credentials/connection/BearerConnectionCredentialVendorTest.java
@@ -0,0 +1,109 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.polaris.service.credentials.connection;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import java.time.Instant;
+import java.util.Map;
+import org.apache.polaris.core.connection.AuthenticationParametersDpo;
+import org.apache.polaris.core.connection.BearerAuthenticationParametersDpo;
+import org.apache.polaris.core.connection.ConnectionConfigInfoDpo;
+import 
org.apache.polaris.core.connection.iceberg.IcebergRestConnectionConfigInfoDpo;
+import org.apache.polaris.core.credentials.connection.CatalogAccessProperty;
+import org.apache.polaris.core.credentials.connection.ConnectionCredentials;
+import org.apache.polaris.core.secrets.SecretReference;
+import org.apache.polaris.core.secrets.UserSecretsManager;
+import org.assertj.core.api.Assertions;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+/** Tests for {@link BearerConnectionCredentialVendor}. */
+public class BearerConnectionCredentialVendorTest {
+
+  private BearerConnectionCredentialVendor bearerVendor;
+  private UserSecretsManager mockSecretsManager;
+
+  @BeforeEach
+  void setup() {
+    mockSecretsManager = mock(UserSecretsManager.class);
+    bearerVendor = new BearerConnectionCredentialVendor(mockSecretsManager);
+  }
+
+  @Test
+  public void testGetConnectionCredentials() {
+    // Setup
+    SecretReference bearerTokenRef =
+        new SecretReference("urn:polaris-secret:test:bearer-token", Map.of());
+    
when(mockSecretsManager.readSecret(bearerTokenRef)).thenReturn("my-bearer-token-value");
+
+    BearerAuthenticationParametersDpo authParams =
+        new BearerAuthenticationParametersDpo(bearerTokenRef);
+
+    IcebergRestConnectionConfigInfoDpo connectionConfig =
+        new IcebergRestConnectionConfigInfoDpo(
+            "https://catalog.example.com";, authParams, null, "test-catalog");
+
+    // Execute
+    ConnectionCredentials credentials = 
bearerVendor.getConnectionCredentials(connectionConfig);
+
+    // Verify - only bearer token is provided
+    Assertions.assertThat(credentials.credentials())
+        .hasSize(1)
+        .containsEntry(
+            CatalogAccessProperty.BEARER_TOKEN.getPropertyName(), 
"my-bearer-token-value");
+    
Assertions.assertThat(credentials.expiresAt()).contains(Instant.ofEpochMilli(Long.MAX_VALUE));
+  }
+
+  @Test
+  public void testGetConnectionCredentialsWithWrongAuthType() {
+    // Setup - use a mock with wrong authentication type
+    ConnectionConfigInfoDpo mockConfig = mock(ConnectionConfigInfoDpo.class);
+    AuthenticationParametersDpo mockAuthParams = 
mock(AuthenticationParametersDpo.class);
+
+    when(mockConfig.getAuthenticationParameters()).thenReturn(mockAuthParams);
+
+    // Execute & Verify - should fail precondition check
+    Assertions.assertThatThrownBy(() -> 
bearerVendor.getConnectionCredentials(mockConfig))
+        .isInstanceOf(IllegalArgumentException.class)
+        .hasMessageContaining("Expected BearerAuthenticationParametersDpo");
+  }
+
+  @Test
+  public void testGetConnectionCredentialsWithInvalidSecretReference() {
+    // Setup - secret reference that doesn't exist
+    SecretReference invalidSecretRef =
+        new SecretReference("urn:polaris-secret:test:non-existent", Map.of());
+    when(mockSecretsManager.readSecret(invalidSecretRef))
+        .thenThrow(new RuntimeException("Secret not found"));
+
+    BearerAuthenticationParametersDpo authParams =
+        new BearerAuthenticationParametersDpo(invalidSecretRef);
+
+    IcebergRestConnectionConfigInfoDpo connectionConfig =
+        new IcebergRestConnectionConfigInfoDpo(
+            "https://catalog.example.com";, authParams, null, "test-catalog");
+
+    // Execute & Verify - should propagate the exception from secrets manager
+    Assertions.assertThatThrownBy(() -> 
bearerVendor.getConnectionCredentials(connectionConfig))
+        .isInstanceOf(RuntimeException.class)
+        .hasMessageContaining("Secret not found");
+  }
+}
diff --git 
a/runtime/service/src/test/java/org/apache/polaris/service/credentials/connection/ImplicitConnectionCredentialVendorTest.java
 
b/runtime/service/src/test/java/org/apache/polaris/service/credentials/connection/ImplicitConnectionCredentialVendorTest.java
new file mode 100644
index 000000000..a2c817671
--- /dev/null
+++ 
b/runtime/service/src/test/java/org/apache/polaris/service/credentials/connection/ImplicitConnectionCredentialVendorTest.java
@@ -0,0 +1,74 @@
+/*
+ * 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.polaris.service.credentials.connection;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import java.time.Instant;
+import org.apache.polaris.core.connection.AuthenticationParametersDpo;
+import org.apache.polaris.core.connection.ConnectionConfigInfoDpo;
+import org.apache.polaris.core.connection.ImplicitAuthenticationParametersDpo;
+import 
org.apache.polaris.core.connection.iceberg.IcebergRestConnectionConfigInfoDpo;
+import org.apache.polaris.core.credentials.connection.ConnectionCredentials;
+import org.assertj.core.api.Assertions;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+/** Tests for {@link ImplicitConnectionCredentialVendor}. */
+public class ImplicitConnectionCredentialVendorTest {
+
+  private ImplicitConnectionCredentialVendor implicitVendor;
+
+  @BeforeEach
+  void setup() {
+    implicitVendor = new ImplicitConnectionCredentialVendor();
+  }
+
+  @Test
+  public void testGetConnectionCredentials() {
+    // Setup
+    ImplicitAuthenticationParametersDpo authParams = new 
ImplicitAuthenticationParametersDpo();
+
+    IcebergRestConnectionConfigInfoDpo connectionConfig =
+        new IcebergRestConnectionConfigInfoDpo(
+            "https://catalog.example.com";, authParams, null, "test-catalog");
+
+    // Execute
+    ConnectionCredentials credentials = 
implicitVendor.getConnectionCredentials(connectionConfig);
+
+    // Verify - no credentials are provided, but expiration is set to infinite
+    Assertions.assertThat(credentials.credentials()).isEmpty();
+    
Assertions.assertThat(credentials.expiresAt()).contains(Instant.ofEpochMilli(Long.MAX_VALUE));
+  }
+
+  @Test
+  public void testGetConnectionCredentialsWithWrongAuthType() {
+    // Setup - use a mock with wrong authentication type
+    ConnectionConfigInfoDpo mockConfig = mock(ConnectionConfigInfoDpo.class);
+    AuthenticationParametersDpo mockAuthParams = 
mock(AuthenticationParametersDpo.class);
+
+    when(mockConfig.getAuthenticationParameters()).thenReturn(mockAuthParams);
+
+    // Execute & Verify - should fail precondition check
+    Assertions.assertThatThrownBy(() -> 
implicitVendor.getConnectionCredentials(mockConfig))
+        .isInstanceOf(IllegalArgumentException.class)
+        .hasMessageContaining("Expected ImplicitAuthenticationParametersDpo");
+  }
+}
diff --git 
a/runtime/service/src/test/java/org/apache/polaris/service/credentials/connection/OAuthClientCredentialVendorTest.java
 
b/runtime/service/src/test/java/org/apache/polaris/service/credentials/connection/OAuthClientCredentialVendorTest.java
new file mode 100644
index 000000000..f60fd8151
--- /dev/null
+++ 
b/runtime/service/src/test/java/org/apache/polaris/service/credentials/connection/OAuthClientCredentialVendorTest.java
@@ -0,0 +1,116 @@
+/*
+ * 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.polaris.service.credentials.connection;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import java.time.Instant;
+import java.util.List;
+import java.util.Map;
+import org.apache.polaris.core.connection.AuthenticationParametersDpo;
+import org.apache.polaris.core.connection.ConnectionConfigInfoDpo;
+import org.apache.polaris.core.connection.OAuthClientCredentialsParametersDpo;
+import 
org.apache.polaris.core.connection.iceberg.IcebergRestConnectionConfigInfoDpo;
+import org.apache.polaris.core.credentials.connection.CatalogAccessProperty;
+import org.apache.polaris.core.credentials.connection.ConnectionCredentials;
+import org.apache.polaris.core.secrets.SecretReference;
+import org.apache.polaris.core.secrets.UserSecretsManager;
+import org.assertj.core.api.Assertions;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+/** Tests for {@link OAuthClientCredentialVendor}. */
+public class OAuthClientCredentialVendorTest {
+
+  private OAuthClientCredentialVendor oauthVendor;
+  private UserSecretsManager mockSecretsManager;
+
+  @BeforeEach
+  void setup() {
+    mockSecretsManager = mock(UserSecretsManager.class);
+    oauthVendor = new OAuthClientCredentialVendor(mockSecretsManager);
+  }
+
+  @Test
+  public void testGetConnectionCredentials() {
+    // Setup
+    SecretReference clientSecretRef =
+        new SecretReference("urn:polaris-secret:test:oauth-client-secret", 
Map.of());
+    
when(mockSecretsManager.readSecret(clientSecretRef)).thenReturn("my-client-secret");
+
+    OAuthClientCredentialsParametersDpo authParams =
+        new OAuthClientCredentialsParametersDpo(
+            "https://auth.example.com/token";,
+            "my-client-id",
+            clientSecretRef,
+            List.of("catalog", "read:data"));
+
+    IcebergRestConnectionConfigInfoDpo connectionConfig =
+        new IcebergRestConnectionConfigInfoDpo(
+            "https://catalog.example.com";, authParams, null, "test-catalog");
+
+    // Execute
+    ConnectionCredentials credentials = 
oauthVendor.getConnectionCredentials(connectionConfig);
+
+    // Verify - only OAuth credential is provided
+    Assertions.assertThat(credentials.credentials())
+        .hasSize(1)
+        .containsEntry(
+            CatalogAccessProperty.OAUTH2_CREDENTIAL.getPropertyName(),
+            "my-client-id:my-client-secret");
+    
Assertions.assertThat(credentials.expiresAt()).contains(Instant.ofEpochMilli(Long.MAX_VALUE));
+  }
+
+  @Test
+  public void testGetConnectionCredentialsWithWrongAuthType() {
+    // Setup - use a mock with wrong authentication type
+    ConnectionConfigInfoDpo mockConfig = mock(ConnectionConfigInfoDpo.class);
+    AuthenticationParametersDpo mockAuthParams = 
mock(AuthenticationParametersDpo.class);
+
+    when(mockConfig.getAuthenticationParameters()).thenReturn(mockAuthParams);
+
+    // Execute & Verify - should fail precondition check
+    Assertions.assertThatThrownBy(() -> 
oauthVendor.getConnectionCredentials(mockConfig))
+        .isInstanceOf(IllegalArgumentException.class)
+        .hasMessageContaining("Expected OAuthClientCredentialsParametersDpo");
+  }
+
+  @Test
+  public void testGetConnectionCredentialsWithInvalidSecretReference() {
+    // Setup - secret reference that doesn't exist
+    SecretReference invalidSecretRef =
+        new SecretReference("urn:polaris-secret:test:non-existent", Map.of());
+    when(mockSecretsManager.readSecret(invalidSecretRef))
+        .thenThrow(new RuntimeException("Secret not found"));
+
+    OAuthClientCredentialsParametersDpo authParams =
+        new OAuthClientCredentialsParametersDpo(
+            "https://auth.example.com/token";, "my-client-id", 
invalidSecretRef, List.of("catalog"));
+
+    IcebergRestConnectionConfigInfoDpo connectionConfig =
+        new IcebergRestConnectionConfigInfoDpo(
+            "https://catalog.example.com";, authParams, null, "test-catalog");
+
+    // Execute & Verify - should propagate the exception from secrets manager
+    Assertions.assertThatThrownBy(() -> 
oauthVendor.getConnectionCredentials(connectionConfig))
+        .isInstanceOf(RuntimeException.class)
+        .hasMessageContaining("Secret not found");
+  }
+}
diff --git 
a/runtime/service/src/testFixtures/java/org/apache/polaris/service/TestServices.java
 
b/runtime/service/src/testFixtures/java/org/apache/polaris/service/TestServices.java
index a77057274..b58cdd771 100644
--- 
a/runtime/service/src/testFixtures/java/org/apache/polaris/service/TestServices.java
+++ 
b/runtime/service/src/testFixtures/java/org/apache/polaris/service/TestServices.java
@@ -278,7 +278,6 @@ public record TestServices(
               resolverFactory,
               resolutionManifestFactory,
               metaStoreManager,
-              userSecretsManager,
               credentialManager,
               authorizer,
               new DefaultCatalogPrefixParser(),


Reply via email to