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

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


The following commit(s) were added to refs/heads/main by this push:
     new 16db87bf6f Feature/trino connector oauth2 user forwarding (#12045)
16db87bf6f is described below

commit 16db87bf6f4c8c7c0b3aa3791043e821d03aa64f
Author: Mark Hoerth <[email protected]>
AuthorDate: Mon Aug 17 00:31:12 2026 -0700

    Feature/trino connector oauth2 user forwarding (#12045)
    
    ### Summary
    Adds per-user authorization and credential vending support to the
    Gravitino Trino connector for Iceberg catalogs backed by the Iceberg
    REST Catalog (IRC).
    Changes
    Two commits:
    
    * oauth2 per-user token forwarding — Extends the connector's session
    forwarding (forwardUser=true) to support authType=oauth2, not just
    simple. When enabled, the end user's forwarded IdP token is presented to
    the Gravitino server, so authorization runs against the real end user
    instead of a shared service identity. Adds StaticUserTokenProvider,
    widens the auth guards in GravitinoConnector and GravitinoAuthProvider,
    and keys the per-user client cache by auth type.
    * per-user credential vending for IRC-backed Iceberg — Configures the
    generated Trino Iceberg REST catalog so that an IRC-backed Gravitino
    catalog requests per-user, per-table vended credentials. Sets
    security=OAUTH2, session=USER, and vended-credentials-enabled=true, and
    maps the warehouse through, so Trino's Iceberg REST client forwards the
    end-user token to the IRC and obtains scoped storage credentials over
    the REST protocol.
    
    ### Testing
    Unit tests added for the oauth2 forwarding path
    (TestGravitinoAuthProvider) and the REST backend config mapping
    (TestIcebergCatalogPropertyConverter). End-to-end verification against a
    live IRC is in progress.
    
    ### Motivation
    Trino is a primary query engine for enterprise customers who require
    real per-user authorization and scoped credentials rather than a shared
    service identity.
    
    ---------
    
    Co-authored-by: Mark Hoerth <[email protected]>
    Co-authored-by: yuhui <[email protected]>
    Co-authored-by: Claude Opus 5 (1M context) <[email protected]>
---
 docs/trino-connector/authentication.md             | 211 +++++++++++++++++----
 .../service/IcebergCatalogWrapperManager.java      |   4 +
 .../authentication/AuthenticationFilter.java       |  11 ++
 .../trino/connector/GravitinoConnector.java        |  79 ++++++--
 .../iceberg/IcebergCatalogPropertyConverter.java   |  12 +-
 .../connector/security/GravitinoAuthProvider.java  |  58 +++++-
 .../security/StaticUserTokenProvider.java          |  54 ++++++
 .../TestGravitinoConnectorForwardUser.java         | 171 ++++++++++++++++-
 .../TestIcebergCatalogPropertyConverter.java       |  27 +++
 .../security/TestGravitinoAuthProvider.java        |  59 ++++++
 10 files changed, 620 insertions(+), 66 deletions(-)

diff --git a/docs/trino-connector/authentication.md 
b/docs/trino-connector/authentication.md
index 5ec6de6018..9793e7749f 100644
--- a/docs/trino-connector/authentication.md
+++ b/docs/trino-connector/authentication.md
@@ -11,6 +11,8 @@ The Gravitino Trino connector supports authenticating to the 
Gravitino server us
 
 If `gravitino.client.authType` is not set, the connector operates in 
no-authentication mode and connects to the Gravitino server without any 
credentials.
 
+## Authentication Types
+
 ### Simple Authentication
 
 Simple authentication uses a username to authenticate with the Gravitino 
server.
@@ -90,32 +92,6 @@ gravitino.client.oauth2.scope=gravitino
 | `gravitino.client.oauth2.path`       | OAuth2 token endpoint path            
                          | (none)        | Yes if authType is `oauth2` |
 | `gravitino.client.oauth2.scope`      | OAuth2 scope                          
                          | (none)        | Yes if authType is `oauth2` |
 
-### Kerberos Authentication
-
-Kerberos authentication uses Kerberos tickets to authenticate with the 
Gravitino server.
-
-**Configuration in `etc/catalog/gravitino.properties`:**
-
-```properties
-connector.name=gravitino
-gravitino.metalake=metalake
-gravitino.uri=http://localhost:8090
-
-# Kerberos authentication with keytab
-gravitino.client.authType=kerberos
-gravitino.client.kerberos.principal=user@REALM
-gravitino.client.kerberos.keytabFilePath=/path/to/user.keytab
-```
-
-**Configuration properties:**
-
-| Property                                   | Description                     
                                | Default value | Required                      
          |
-|--------------------------------------------|-----------------------------------------------------------------|---------------|-----------------------------------------|
-| `gravitino.client.authType`                | Authentication type: `simple`, 
`basic`, `oauth2`, or `kerberos` | (none)        | Yes (to enable Kerberos)     
           |
-| `gravitino.client.kerberos.principal`      | Kerberos principal              
                                | (none)        | Yes if authType is `kerberos` 
          |
-| `gravitino.client.kerberos.keytabFilePath` | Path to keytab file             
                                | (none)        | No (uses ticket cache if not 
specified) |
-
-
 ### Example: Connecting to OAuth-Protected Gravitino Server
 
 This example shows how to configure the Trino connector to connect to a 
Gravitino server protected by OAuth authentication.
@@ -151,11 +127,36 @@ gravitino.client.oauth2.scope=test
 SHOW CATALOGS;
 ```
 
-### Session Credential Forwarding
+### Kerberos Authentication
+
+Kerberos authentication uses Kerberos tickets to authenticate with the 
Gravitino server.
+
+**Configuration in `etc/catalog/gravitino.properties`:**
+
+```properties
+connector.name=gravitino
+gravitino.metalake=metalake
+gravitino.uri=http://localhost:8090
+
+# Kerberos authentication with keytab
+gravitino.client.authType=kerberos
+gravitino.client.kerberos.principal=user@REALM
+gravitino.client.kerberos.keytabFilePath=/path/to/user.keytab
+```
+
+**Configuration properties:**
+
+| Property                                     | Description                   
                                      | Default value   | Required              
                    | Since version   |
+|----------------------------------------------|---------------------------------------------------------------------|-----------------|-------------------------------------------|-----------------|
+| `gravitino.client.authType`                  | Authentication type: 
`simple`, `basic`, `oauth2`, or `kerberos`     | (none)          | Yes (to 
enable Kerberos)                  | 1.3.0           |
+| `gravitino.client.kerberos.principal`        | Kerberos principal            
                                      | (none)          | Yes if authType is 
`kerberos`             | 1.3.0           |
+| `gravitino.client.kerberos.keytabFilePath`   | Path to keytab file           
                                      | (none)          | No (uses ticket cache 
if not specified)   | 1.3.0           |
 
-Setting `gravitino.client.session.forwardUser=true` with `authType=simple` 
creates a dedicated Gravitino client per Trino session user, so each user is 
visible in the Gravitino audit log instead of the shared `gravitino.user`.
+## Session Credential Forwarding
 
-**Configuration:**
+Setting `gravitino.client.session.forwardUser=true` creates a dedicated 
Gravitino client per Trino session user, so each user is visible in the 
Gravitino audit log instead of the shared `gravitino.user` or service identity. 
It is supported with `authType=simple` and `authType=oauth2`.
+
+**Configuration (`authType=simple`):**
 
 ```properties
 connector.name=gravitino
@@ -166,22 +167,160 @@ gravitino.client.authType=simple
 gravitino.client.session.forwardUser=true
 ```
 
+With `authType=simple`, the Trino session username is forwarded to Gravitino 
as the simple-auth identity.
+
+**Configuration (`authType=oauth2`):**
+
+```properties
+connector.name=gravitino
+gravitino.metalake=metalake
+gravitino.uri=http://localhost:8090
+
+gravitino.client.authType=oauth2
+gravitino.client.oauth2.serverUri=http://oauth-server:8080
+gravitino.client.oauth2.credential=client_id:client_secret
+gravitino.client.oauth2.path=oauth2/token
+gravitino.client.oauth2.scope=gravitino
+gravitino.client.session.forwardUser=true
+```
+
+With `authType=oauth2`, the end user's IdP access token is presented to 
Gravitino directly instead of the shared client-credentials identity. This 
requires the Trino coordinator to populate the session's extra-credentials with 
the caller's access token under the key `token`; the connector reads it from 
there, and `buildForSession` fails with a clear error if it's missing.
+
+Whether the coordinator can populate this extra-credential depends on the 
Trino distribution:
+
+- **Starburst Enterprise** supports this via 
`http-server.authentication.type=DELEGATED-OAUTH2` — see [OAuth 2.0 token 
pass-through](https://docs.starburst.io/latest/security/oauth2-passthrough.html).
+- **Open-source Trino does not support this yet.** There is no equivalent 
coordinator-side mechanism to forward the caller's OAuth2 token into the 
connector session; see [trinodb/trino discussion 
#24403](https://github.com/trinodb/trino/discussions/24403) and [issue 
#27917](https://github.com/trinodb/trino/issues/27917) tracking this feature 
request upstream.
+
+The `gravitino.client.oauth2.*` properties above still configure the shared 
bootstrap/admin client used for catalog discovery — they are unrelated to the 
per-user forwarded token.
+
+For an Iceberg catalog with `catalog-backend=rest` (backed by an Iceberg REST 
Catalog), the connector does not set 
`iceberg.rest-catalog.security`/`iceberg.rest-catalog.session` on its own — 
that catalog's own `gravitino.client.*` config is unrelated to how its 
underlying Iceberg REST catalog authenticates. To also forward the end user's 
token to the REST catalog itself, set 
`trino.bypass.iceberg.rest-catalog.security=OAUTH2` and 
`trino.bypass.iceberg.rest-catalog.session=USER` explicitl [...]
+
 **Configuration properties:**
 
-| Property                                                  | Description      
                                                                          | 
Default value | Required |
-|-----------------------------------------------------------|--------------------------------------------------------------------------------------------|---------------|----------|
-| `gravitino.client.session.forwardUser`                    | When `true` with 
`authType=simple`, forwards the Trino session user to Gravitino per-query | 
`false`       | No       |
-| `gravitino.client.session.cache.maxSize`                  | Maximum number 
of per-user sessions to keep in the cache                                   | 
`500`         | No       |
-| `gravitino.client.session.cache.expireAfterAccessSeconds` | Seconds before 
an idle per-user session is evicted from the cache                          | 
`3600`        | No       |
+| Property                                                     | Description   
                                                                                
 | Default value   | Required   | Since version   |
+|--------------------------------------------------------------|--------------------------------------------------------------------------------------------------|-----------------|------------|-----------------|
+| `gravitino.client.session.forwardUser`                       | When `true` 
with `authType=simple` or `authType=oauth2`, forwards the Trino session 
user/token to Gravitino per-query   | `false`         | No         | 1.3.0      
     |
+| `gravitino.client.session.cache.maxSize`                     | Maximum 
number of per-user sessions to keep in the cache                                
       | `500`           | No         | 1.3.0           |
+| `gravitino.client.session.cache.expireAfterAccessSeconds`    | Seconds 
before an idle per-user session is evicted from the cache                       
       | `3600`          | No         | 1.3.0           |
+
+### Example: OAuth2 Per-User Token Forwarding
+
+This example walks through a full setup where each Trino user's own OAuth2 
access token is
+forwarded to Gravitino and to an Iceberg REST catalog (IRC), instead of a 
single shared service
+identity.
+
+**1. Trino coordinator: forward the logged-in user's token to connectors.** 
This is the
+prerequisite that makes `authType=oauth2` forwarding possible at all — the 
coordinator must
+populate the session's extra-credentials with the caller's access token under 
the key `token`.
+
+- **Starburst Enterprise**: set the following in `etc/config.properties`:
+
+  ```properties
+  http-server.authentication.type=DELEGATED-OAUTH2
+  ```
+
+  See [OAuth 2.0 token 
pass-through](https://docs.starburst.io/latest/security/oauth2-passthrough.html)
+  for details, including its limitation that pass-through tokens are not 
refreshed and must
+  outlive the query.
+
+- **Open-source Trino**: there is currently no equivalent coordinator setting. 
Track
+  [trinodb/trino discussion 
#24403](https://github.com/trinodb/trino/discussions/24403) and
+  [issue #27917](https://github.com/trinodb/trino/issues/27917) for this 
feature request. Until
+  it lands upstream, this connector's `authType=oauth2` forwardUser path 
requires a Trino
+  distribution that provides this extra-credential itself.
+
+**2. Gravitino server: enable OAuth2** (in `conf/gravitino.conf`):
+
+```properties
+gravitino.authenticators=oauth
+gravitino.authenticator.oauth.serviceAudience=account
+gravitino.authenticator.oauth.jwksUri=http://your-idp/realms/gravitino/protocol/openid-connect/certs
+gravitino.authenticator.oauth.tokenValidatorClass=org.apache.gravitino.server.authentication.JwksTokenValidator
+gravitino.authenticator.oauth.principalFields=preferred_username,email,sub
+```
+
+**3. Trino connector: enable OAuth2 forwarding** (in 
`etc/catalog/gravitino.properties`):
+
+```properties
+connector.name=gravitino
+gravitino.metalake=my_metalake
+gravitino.uri=http://localhost:8090
+
+gravitino.client.authType=oauth2
+gravitino.client.oauth2.serverUri=http://your-idp
+gravitino.client.oauth2.credential=service-account-id:service-account-secret
+gravitino.client.oauth2.path=realms/gravitino/protocol/openid-connect/token
+gravitino.client.oauth2.scope=email
+gravitino.client.session.forwardUser=true
+```
+
+The `gravitino.client.oauth2.*` properties configure the shared service 
identity used for catalog
+discovery; the per-user forwarded token (from step 1) is what each query 
actually authenticates
+with once `forwardUser=true`.
+
+**4. Create the metalake and catalog.** Create the metalake `my_metalake` 
first (via the
+Gravitino REST API, SDK, or CLI — see
+[Manage metalakes](../manage-metalake-using-gravitino.md#create-a-metalake)), 
then create a
+REST-backed Iceberg catalog under it from the Trino CLI using the
+`gravitino.system.create_catalog` procedure. To also forward the end user's 
token to the Iceberg
+REST catalog (IRC) itself, set 
`trino.bypass.iceberg.rest-catalog.security=OAUTH2` and
+`trino.bypass.iceberg.rest-catalog.session=USER` on the catalog, alongside its 
bootstrap
+`trino.bypass.iceberg.rest-catalog.oauth2.*` credentials:
+
+```sql
+call gravitino.system.create_catalog(
+    'my_catalog',
+    'lakehouse-iceberg',
+    map(
+        array['uri', 'catalog-backend', 'warehouse',
+          'trino.bypass.iceberg.rest-catalog.security', 
'trino.bypass.iceberg.rest-catalog.session',
+          'trino.bypass.iceberg.rest-catalog.oauth2.credential', 
'trino.bypass.iceberg.rest-catalog.oauth2.scope',
+          'trino.bypass.iceberg.rest-catalog.oauth2.server-uri'
+        ],
+        array['http://irc-host:9001/iceberg', 'rest', 'my_catalog',
+          'OAUTH2', 'USER',
+          'service-account-id:service-account-secret', 'email',
+          'http://your-idp/realms/gravitino/protocol/openid-connect/token'
+        ]
+    )
+);
+```
+
+This call itself runs with the connector's own shared service identity, not 
any forwarded user
+token — `forwardUser` only affects `SELECT`/`SHOW`-style queries against the 
catalog afterward,
+not catalog registration itself. `create_catalog` both creates the catalog in 
Gravitino and loads
+it into Trino as its own top-level catalog — not as a schema nested under a 
single `gravitino`
+catalog. If the two `trino.bypass.iceberg.rest-catalog.*` properties above are 
omitted, the REST
+catalog keeps its own default security setting, independent of
+`gravitino.client.session.forwardUser`, and the end user's token never reaches 
the IRC.
+
+**5. Query as a specific user.** With a real OIDC login flow, Trino populates 
the forwarded token
+automatically after the user signs in — this automatic population is the part 
that requires
+Starburst's DELEGATED-OAUTH2 (step 1). For manual testing on any Trino 
distribution, including
+open-source Trino, the same extra-credential can instead be set directly on 
the CLI, independent
+of how the coordinator is configured:
+
+```shell
+trino --server http://localhost:8080 \
+  --user alice \
+  --extra-credential token=<alice-idp-access-token> \
+  --execute "SHOW SCHEMAS IN my_catalog"
+```
+
+Gravitino sees this request as `alice`, not the shared service identity — 
`alice`'s own
+privileges apply, and a request with a missing or invalid token is rejected 
before it reaches the
+catalog. Because `my_catalog` was created with 
`trino.bypass.iceberg.rest-catalog.session=USER`
+in step 4, the same forwarded token also reaches the IRC directly, so per-user 
authorization
+applies consistently whether Trino talks to Gravitino's native API or straight 
to the IRC.
 
-### Notes
+## Notes
 
 - The Gravitino server must be configured with the corresponding 
authentication mechanism enabled.
 - For OAuth2 authentication, ensure the OAuth2 server is accessible from the 
Trino coordinator and workers.
 - For Kerberos authentication, ensure the Kerberos configuration is properly 
set up on all Trino nodes.
 - Authentication configuration is passed through the `gravitino.client.*` 
prefix to the underlying Gravitino Java client.
 
-### See Also
+## See Also
 
 - [Gravitino Server Authentication 
Configuration](../security/how-to-authenticate.md)
 - [Local users and groups](../security/local-users-and-groups.md)
diff --git 
a/iceberg/iceberg-rest-server/src/main/java/org/apache/gravitino/iceberg/service/IcebergCatalogWrapperManager.java
 
b/iceberg/iceberg-rest-server/src/main/java/org/apache/gravitino/iceberg/service/IcebergCatalogWrapperManager.java
index c524d074fa..d5a2d27206 100644
--- 
a/iceberg/iceberg-rest-server/src/main/java/org/apache/gravitino/iceberg/service/IcebergCatalogWrapperManager.java
+++ 
b/iceberg/iceberg-rest-server/src/main/java/org/apache/gravitino/iceberg/service/IcebergCatalogWrapperManager.java
@@ -103,6 +103,10 @@ public class IcebergCatalogWrapperManager implements 
AutoCloseable {
   }
 
   public CatalogWrapperForREST getCatalogWrapper(String catalogName) {
+    if (LOG.isDebugEnabled()) {
+      boolean cacheHit = catalogWrapperCache.getIfPresent(catalogName) != null;
+      LOG.debug("getCatalogWrapper catalogName={} cacheHit={}", catalogName, 
cacheHit);
+    }
     CatalogWrapperForREST catalogWrapperForREST =
         catalogWrapperCache.get(catalogName, k -> 
createCatalogWrapper(catalogName));
     // Reload conf to reset UserGroupInformation or icebergTableOps will 
always use
diff --git 
a/server-common/src/main/java/org/apache/gravitino/server/authentication/AuthenticationFilter.java
 
b/server-common/src/main/java/org/apache/gravitino/server/authentication/AuthenticationFilter.java
index 8fa379922b..6f75837212 100644
--- 
a/server-common/src/main/java/org/apache/gravitino/server/authentication/AuthenticationFilter.java
+++ 
b/server-common/src/main/java/org/apache/gravitino/server/authentication/AuthenticationFilter.java
@@ -43,9 +43,13 @@ import org.apache.gravitino.exceptions.UnauthorizedException;
 import org.apache.gravitino.server.web.HealthCheckPathMatcher;
 import org.apache.gravitino.server.web.ObjectMapperProvider;
 import org.apache.gravitino.utils.PrincipalUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 public class AuthenticationFilter implements Filter {
 
+  private static final Logger LOG = 
LoggerFactory.getLogger(AuthenticationFilter.class);
+
   private final List<Authenticator> filterAuthenticators;
 
   /**
@@ -100,6 +104,13 @@ public class AuthenticationFilter implements Filter {
           }
         }
       }
+      if (LOG.isDebugEnabled()) {
+        LOG.debug(
+            "uri={} hasAuthHeader={} principal={}",
+            req.getRequestURI(),
+            authData != null,
+            principal == null ? "null" : principal.getName());
+      }
       if (principal == null) {
         throw new UnauthorizedException("The provided credentials did not 
support");
       }
diff --git 
a/trino-connector/trino-connector/src/main/java/org/apache/gravitino/trino/connector/GravitinoConnector.java
 
b/trino-connector/trino-connector/src/main/java/org/apache/gravitino/trino/connector/GravitinoConnector.java
index 01d419d1fe..e038f9671e 100644
--- 
a/trino-connector/trino-connector/src/main/java/org/apache/gravitino/trino/connector/GravitinoConnector.java
+++ 
b/trino-connector/trino-connector/src/main/java/org/apache/gravitino/trino/connector/GravitinoConnector.java
@@ -18,13 +18,16 @@
  */
 package org.apache.gravitino.trino.connector;
 
+import static io.trino.spi.StandardErrorCode.GENERIC_INTERNAL_ERROR;
 import static io.trino.spi.StandardErrorCode.NOT_SUPPORTED;
 import static io.trino.spi.StandardErrorCode.PERMISSION_DENIED;
 
+import com.google.common.annotations.VisibleForTesting;
 import com.google.common.base.Preconditions;
 import com.google.common.cache.Cache;
 import com.google.common.cache.CacheBuilder;
 import com.google.common.cache.RemovalNotification;
+import com.google.common.util.concurrent.UncheckedExecutionException;
 import io.trino.spi.TrinoException;
 import io.trino.spi.connector.Connector;
 import io.trino.spi.connector.ConnectorAccessControl;
@@ -225,37 +228,82 @@ public class GravitinoConnector implements Connector {
     catalogConnectorContext.close();
   }
 
-  private CatalogConnectorMetadata resolveSessionMetadata(ConnectorSession 
session) {
-    String credKey = "simple:" + session.getUser();
+  @VisibleForTesting
+  CatalogConnectorMetadata resolveSessionMetadata(ConnectorSession session) {
+    String authType =
+        catalogConnectorContext
+            .getConfig()
+            .getClientConfig()
+            .getOrDefault(GravitinoAuthProvider.AUTH_TYPE_KEY, "simple");
+    String credentialKey =
+        catalogConnectorContext
+            .getConfig()
+            .getClientConfig()
+            .getOrDefault(
+                GravitinoAuthProvider.USER_TOKEN_CREDENTIAL_KEY,
+                GravitinoAuthProvider.DEFAULT_USER_TOKEN_CREDENTIAL_KEY);
+    String token = 
session.getIdentity().getExtraCredentials().get(credentialKey);
+    String credKey = sessionCacheKey(authType, session.getUser(), token);
     try {
       return perUserSessionCache.get(
               credKey,
               () -> {
-                GravitinoAdminClient userClient =
-                    GravitinoAuthProvider.buildForSession(
-                        catalogConnectorContext.getConfig(), session);
+                GravitinoAdminClient userClient = buildAuthClient(session);
                 GravitinoMetalake userMetalake =
                     
userClient.loadMetalake(catalogConnectorContext.getMetalake().name());
                 return new UserSession(
                     userClient, new CatalogConnectorMetadata(userMetalake, 
catalogIdentifier));
               })
           .metadata;
-    } catch (ExecutionException e) {
-      Throwable cause = e.getCause();
+    } catch (ExecutionException | UncheckedExecutionException e) {
+      Throwable cause = e.getCause() == null ? e : e.getCause();
       LOG.warn(
-          "Failed to create per-user Gravitino client for user '{}': {}",
-          session.getUser(),
-          cause.getMessage());
+          "Failed to create per-user Gravitino client for user '{}'", 
session.getUser(), cause);
+      if (cause instanceof TrinoException) {
+        // Already carries a specific Trino error code (e.g. from 
buildForSession); re-wrapping
+        // would swallow it.
+        throw (TrinoException) cause;
+      }
+      if (cause instanceof IllegalArgumentException
+          || cause instanceof UnsupportedOperationException) {
+        throw new TrinoException(
+            PERMISSION_DENIED,
+            "Failed to authenticate user '"
+                + session.getUser()
+                + "' with Gravitino: "
+                + cause.getMessage(),
+            cause);
+      }
       throw new TrinoException(
-          PERMISSION_DENIED,
-          "Failed to authenticate user '"
+          GENERIC_INTERNAL_ERROR,
+          "Unexpected error while creating per-user Gravitino client for user 
'"
               + session.getUser()
-              + "' with Gravitino: "
+              + "': "
               + cause.getMessage(),
           cause);
     }
   }
 
+  /**
+   * Builds the per-user Gravitino admin client for a forwarded session. 
Extracted as an overridable
+   * seam so tests can substitute the client-building behavior without needing 
to mock the static
+   * {@link GravitinoAuthProvider#buildForSession}.
+   *
+   * @param session the current Trino connector session
+   * @return the per-user Gravitino admin client
+   */
+  @VisibleForTesting
+  GravitinoAdminClient buildAuthClient(ConnectorSession session) {
+    return 
GravitinoAuthProvider.buildForSession(catalogConnectorContext.getConfig(), 
session);
+  }
+
+  @VisibleForTesting
+  static String sessionCacheKey(String authType, String user, String token) {
+    String tokenPart =
+        StringUtils.isBlank(token) ? "" : ":" + 
Integer.toHexString(token.hashCode());
+    return authType + ":" + user + tokenPart;
+  }
+
   private Cache<String, UserSession> buildSessionCache(GravitinoConfig config) 
{
     Map<String, String> clientConfig = config.getClientConfig();
     String authTypeStr = clientConfig.get(GravitinoAuthProvider.AUTH_TYPE_KEY);
@@ -265,10 +313,11 @@ public class GravitinoConnector implements Connector {
           "gravitino.client.session.forwardUser=true requires 
gravitino.client.authType to be set");
     }
     GravitinoAuthProvider.AuthType authType = 
GravitinoAuthProvider.parseAuthType(authTypeStr);
-    if (authType != GravitinoAuthProvider.AuthType.SIMPLE) {
+    if (authType != GravitinoAuthProvider.AuthType.SIMPLE
+        && authType != GravitinoAuthProvider.AuthType.OAUTH2) {
       throw new TrinoException(
           GravitinoErrorCode.GRAVITINO_ILLEGAL_ARGUMENT,
-          "gravitino.client.session.forwardUser=true only supports 
authType=simple, got: "
+          "gravitino.client.session.forwardUser=true only supports 
authType=simple or oauth2, got: "
               + authTypeStr);
     }
 
diff --git 
a/trino-connector/trino-connector/src/main/java/org/apache/gravitino/trino/connector/catalog/iceberg/IcebergCatalogPropertyConverter.java
 
b/trino-connector/trino-connector/src/main/java/org/apache/gravitino/trino/connector/catalog/iceberg/IcebergCatalogPropertyConverter.java
index 1802c6c64c..d6459b5988 100644
--- 
a/trino-connector/trino-connector/src/main/java/org/apache/gravitino/trino/connector/catalog/iceberg/IcebergCatalogPropertyConverter.java
+++ 
b/trino-connector/trino-connector/src/main/java/org/apache/gravitino/trino/connector/catalog/iceberg/IcebergCatalogPropertyConverter.java
@@ -161,9 +161,13 @@ public class IcebergCatalogPropertyConverter extends 
CatalogPropertyConverter {
           "Missing required property for Rest backend: " + missingProperty);
     }
 
-    Map<String, String> jdbcProperties = new HashMap<>();
-    jdbcProperties.put("iceberg.catalog.type", "rest");
-    jdbcProperties.put("iceberg.rest-catalog.uri", 
properties.get(IcebergConstants.URI));
-    return jdbcProperties;
+    Map<String, String> restProperties = new HashMap<>();
+    restProperties.put("iceberg.catalog.type", "rest");
+    restProperties.put("iceberg.rest-catalog.uri", 
properties.get(IcebergConstants.URI));
+    if (properties.containsKey(IcebergConstants.WAREHOUSE)) {
+      restProperties.put(
+          "iceberg.rest-catalog.warehouse", 
properties.get(IcebergConstants.WAREHOUSE));
+    }
+    return restProperties;
   }
 }
diff --git 
a/trino-connector/trino-connector/src/main/java/org/apache/gravitino/trino/connector/security/GravitinoAuthProvider.java
 
b/trino-connector/trino-connector/src/main/java/org/apache/gravitino/trino/connector/security/GravitinoAuthProvider.java
index 21e2e25604..2f68a9a7b4 100644
--- 
a/trino-connector/trino-connector/src/main/java/org/apache/gravitino/trino/connector/security/GravitinoAuthProvider.java
+++ 
b/trino-connector/trino-connector/src/main/java/org/apache/gravitino/trino/connector/security/GravitinoAuthProvider.java
@@ -19,6 +19,7 @@
 package org.apache.gravitino.trino.connector.security;
 
 import com.google.common.base.Preconditions;
+import io.trino.spi.TrinoException;
 import io.trino.spi.connector.ConnectorSession;
 import java.io.File;
 import java.util.Locale;
@@ -29,6 +30,7 @@ import org.apache.gravitino.client.GravitinoAdminClient;
 import org.apache.gravitino.client.GravitinoClientConfiguration;
 import org.apache.gravitino.client.KerberosTokenProvider;
 import org.apache.gravitino.trino.connector.GravitinoConfig;
+import org.apache.gravitino.trino.connector.GravitinoErrorCode;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -55,6 +57,18 @@ public class GravitinoAuthProvider {
   public static final String FORWARD_SESSION_USER_KEY =
       GravitinoClientConfiguration.GRAVITINO_CLIENT_CONFIG_PREFIX + 
"session.forwardUser";
 
+  /**
+   * Configuration key naming the Trino session extra-credential that carries 
the end user's
+   * forwarded IdP access token. Defaults to {@value 
#DEFAULT_USER_TOKEN_CREDENTIAL_KEY}. Query
+   * engines differ in how they label the forwarded token, so this is 
configurable rather than
+   * fixed.
+   */
+  public static final String USER_TOKEN_CREDENTIAL_KEY =
+      "gravitino.client.session.userTokenCredentialKey";
+
+  /** Default extra-credential name used when {@link 
#USER_TOKEN_CREDENTIAL_KEY} is not set. */
+  public static final String DEFAULT_USER_TOKEN_CREDENTIAL_KEY = "token";
+
   /** Built-in IdP username configuration key for Basic authentication. */
   public static final String BASIC_USERNAME_KEY =
       GravitinoClientConfiguration.GRAVITINO_CLIENT_CONFIG_PREFIX + 
"basic.username";
@@ -165,8 +179,10 @@ public class GravitinoAuthProvider {
    * connector session. This is the entry point for the per-user client cache 
when {@code
    * forwardUser=true}.
    *
-   * <p>Currently only {@code authType=simple} is supported: the Trino session 
username is used as
-   * the Gravitino simple-auth identity.
+   * <p>For {@code authType=simple} the Trino session username is used as the 
Gravitino simple-auth
+   * identity. For {@code authType=oauth2} the end-user IdP token forwarded 
into the connector
+   * session (extra-credential key {@code token}) is presented to Gravitino as 
the bearer token, so
+   * the server authorizes against the end user rather than a shared service 
identity.
    *
    * @param config the Gravitino connector configuration
    * @param session the current Trino connector session
@@ -196,13 +212,38 @@ public class GravitinoAuthProvider {
 
     GravitinoAdminClient.AdminClientBuilder builder = 
GravitinoAdminClient.builder(uri);
 
-    if (authType != AuthType.SIMPLE) {
-      throw new UnsupportedOperationException(
-          "Auth type "
-              + authType
-              + " does not support session forwarding. Only simple is 
supported.");
+    switch (authType) {
+      case SIMPLE:
+        builder.withSimpleAuth(session.getUser());
+        break;
+      case OAUTH2:
+        {
+          String credentialKey =
+              clientConfig.getOrDefault(
+                  USER_TOKEN_CREDENTIAL_KEY, 
DEFAULT_USER_TOKEN_CREDENTIAL_KEY);
+          String userToken = 
session.getIdentity().getExtraCredentials().get(credentialKey);
+          if (StringUtils.isBlank(userToken)) {
+            throw new TrinoException(
+                GravitinoErrorCode.GRAVITINO_ILLEGAL_ARGUMENT,
+                "No forwarded user token found in session extra-credentials 
under key '"
+                    + credentialKey
+                    + "'. The Trino coordinator must populate this 
extra-credential with the "
+                    + "caller's OAuth2 access token, e.g. Starburst 
Enterprise's "
+                    + "http-server.authentication.type=DELEGATED-OAUTH2; 
open-source Trino does not "
+                    + "support this yet. If the coordinator labels the 
forwarded token differently, "
+                    + "set "
+                    + USER_TOKEN_CREDENTIAL_KEY
+                    + " to match.");
+          }
+          builder.withOAuth(new StaticUserTokenProvider(userToken));
+        }
+        break;
+      default:
+        throw new UnsupportedOperationException(
+            "Auth type "
+                + authType
+                + " does not support session forwarding. Only simple and 
oauth2 are supported.");
     }
-    builder.withSimpleAuth(session.getUser());
 
     removeAuthSpecificKeys(clientConfig);
     builder.withClientConfig(clientConfig);
@@ -233,6 +274,7 @@ public class GravitinoAuthProvider {
     clientConfig.remove(KERBEROS_PRINCIPAL_KEY);
     clientConfig.remove(KERBEROS_KEYTAB_FILE_PATH_KEY);
     clientConfig.remove(FORWARD_SESSION_USER_KEY);
+    clientConfig.remove(USER_TOKEN_CREDENTIAL_KEY);
     clientConfig.remove(SESSION_CACHE_MAX_SIZE_KEY);
     clientConfig.remove(SESSION_CACHE_EXPIRE_AFTER_ACCESS_SECONDS_KEY);
   }
diff --git 
a/trino-connector/trino-connector/src/main/java/org/apache/gravitino/trino/connector/security/StaticUserTokenProvider.java
 
b/trino-connector/trino-connector/src/main/java/org/apache/gravitino/trino/connector/security/StaticUserTokenProvider.java
new file mode 100644
index 0000000000..f477a633b3
--- /dev/null
+++ 
b/trino-connector/trino-connector/src/main/java/org/apache/gravitino/trino/connector/security/StaticUserTokenProvider.java
@@ -0,0 +1,54 @@
+/*
+ * 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.gravitino.trino.connector.security;
+
+import com.google.common.base.Preconditions;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.gravitino.client.OAuth2TokenProvider;
+
+/**
+ * An {@link OAuth2TokenProvider} that returns a pre-fetched, already-valid 
access token rather than
+ * minting one via client credentials. Used for per-user session forwarding: 
the end user's IdP
+ * access token, forwarded by Trino into the connector session, is presented 
directly to Gravitino
+ * so the server authorizes against the end user's identity instead of a 
shared service identity.
+ *
+ * <p>The raw token is returned from {@link #getAccessToken()}. The {@code 
Bearer } prefix is added
+ * by {@link OAuth2TokenProvider#getTokenData()}, so the token held here must 
not include it.
+ */
+public final class StaticUserTokenProvider extends OAuth2TokenProvider {
+
+  private final String accessToken;
+
+  /**
+   * Constructs a provider that always returns the given access token.
+   *
+   * @param accessToken the raw bearer token (without the {@code Bearer } 
prefix) to present to
+   *     Gravitino
+   */
+  public StaticUserTokenProvider(String accessToken) {
+    Preconditions.checkArgument(
+        StringUtils.isNotBlank(accessToken), "accessToken must not be blank");
+    this.accessToken = accessToken;
+  }
+
+  @Override
+  protected String getAccessToken() {
+    return accessToken;
+  }
+}
diff --git 
a/trino-connector/trino-connector/src/test/java/org/apache/gravitino/trino/connector/TestGravitinoConnectorForwardUser.java
 
b/trino-connector/trino-connector/src/test/java/org/apache/gravitino/trino/connector/TestGravitinoConnectorForwardUser.java
index 4380942be9..29b91f8ca3 100644
--- 
a/trino-connector/trino-connector/src/test/java/org/apache/gravitino/trino/connector/TestGravitinoConnectorForwardUser.java
+++ 
b/trino-connector/trino-connector/src/test/java/org/apache/gravitino/trino/connector/TestGravitinoConnectorForwardUser.java
@@ -18,8 +18,13 @@
  */
 package org.apache.gravitino.trino.connector;
 
+import static io.trino.spi.StandardErrorCode.GENERIC_INTERNAL_ERROR;
+import static io.trino.spi.StandardErrorCode.PERMISSION_DENIED;
 import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
 import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
+import static org.junit.jupiter.api.Assertions.assertNotSame;
+import static org.junit.jupiter.api.Assertions.assertSame;
 import static org.junit.jupiter.api.Assertions.assertThrows;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.Mockito.mock;
@@ -27,12 +32,18 @@ import static org.mockito.Mockito.when;
 
 import com.google.common.collect.ImmutableMap;
 import io.trino.spi.TrinoException;
+import io.trino.spi.connector.ConnectorSession;
+import io.trino.spi.security.ConnectorIdentity;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.function.Function;
 import org.apache.gravitino.Catalog;
 import org.apache.gravitino.NameIdentifier;
 import org.apache.gravitino.SupportsSchemas;
+import org.apache.gravitino.client.GravitinoAdminClient;
 import org.apache.gravitino.client.GravitinoMetalake;
 import org.apache.gravitino.rel.TableCatalog;
 import org.apache.gravitino.trino.connector.catalog.CatalogConnectorContext;
+import org.apache.gravitino.trino.connector.catalog.CatalogConnectorMetadata;
 import org.apache.gravitino.trino.connector.metadata.GravitinoCatalog;
 import org.apache.gravitino.trino.connector.security.GravitinoAuthProvider;
 import org.junit.jupiter.api.Test;
@@ -63,15 +74,14 @@ class TestGravitinoConnectorForwardUser {
   }
 
   @Test
-  void testForwardUserWithOAuth2AuthTypeThrowsAtConstruction() {
+  void testForwardUserWithOAuth2AuthTypeSucceeds() {
     CatalogConnectorContext ctx =
         mockContextWithConfig(
             ImmutableMap.of(
                 GravitinoAuthProvider.FORWARD_SESSION_USER_KEY, "true",
                 GravitinoAuthProvider.AUTH_TYPE_KEY, "oauth2"));
 
-    TrinoException ex = assertThrows(TrinoException.class, () -> new 
GravitinoConnector(ctx));
-    assertEquals(GravitinoErrorCode.GRAVITINO_ILLEGAL_ARGUMENT.toErrorCode(), 
ex.getErrorCode());
+    assertDoesNotThrow(() -> new GravitinoConnector(ctx));
   }
 
   @Test
@@ -91,6 +101,161 @@ class TestGravitinoConnectorForwardUser {
     assertDoesNotThrow(() -> new GravitinoConnector(ctx));
   }
 
+  @Test
+  void testSessionCacheKeyIsolatesDifferentUsers() {
+    assertNotEquals(
+        GravitinoConnector.sessionCacheKey("oauth2", "alice", "tok"),
+        GravitinoConnector.sessionCacheKey("oauth2", "bob", "tok"));
+  }
+
+  @Test
+  void testSessionCacheKeyIsolatesDifferentAuthTypes() {
+    assertNotEquals(
+        GravitinoConnector.sessionCacheKey("oauth2", "alice", "tok"),
+        GravitinoConnector.sessionCacheKey("simple", "alice", "tok"));
+  }
+
+  @Test
+  void testSessionCacheKeyIsolatesDifferentTokens() {
+    assertNotEquals(
+        GravitinoConnector.sessionCacheKey("oauth2", "alice", "token-a"),
+        GravitinoConnector.sessionCacheKey("oauth2", "alice", "token-b"));
+  }
+
+  @Test
+  void testSessionCacheKeyIsStableForSameUserAuthTypeAndToken() {
+    assertEquals(
+        GravitinoConnector.sessionCacheKey("oauth2", "alice", "tok"),
+        GravitinoConnector.sessionCacheKey("oauth2", "alice", "tok"));
+  }
+
+  @Test
+  void testSessionCacheKeyIgnoresBlankTokenForSimpleAuth() {
+    assertEquals(
+        GravitinoConnector.sessionCacheKey("simple", "alice", null),
+        GravitinoConnector.sessionCacheKey("simple", "alice", ""));
+  }
+
+  @Test
+  void testResolveSessionMetadataBuildsNewClientOnTokenRotation() {
+    CatalogConnectorContext ctx =
+        mockContextWithConfig(
+            ImmutableMap.of(
+                GravitinoAuthProvider.FORWARD_SESSION_USER_KEY, "true",
+                GravitinoAuthProvider.AUTH_TYPE_KEY, "oauth2"));
+    AtomicInteger buildCount = new AtomicInteger();
+    GravitinoConnector connector =
+        newConnectorWithAuthClient(ctx, session -> 
mockAdminClient(ctx.getMetalake(), buildCount));
+
+    CatalogConnectorMetadata first =
+        connector.resolveSessionMetadata(mockSession("alice", "token-a"));
+    CatalogConnectorMetadata second =
+        connector.resolveSessionMetadata(mockSession("alice", "token-a"));
+    CatalogConnectorMetadata third =
+        connector.resolveSessionMetadata(mockSession("alice", "token-b"));
+
+    assertSame(first, second, "same user/token should reuse the cached 
client");
+    assertNotSame(first, third, "a rotated token must not reuse the stale 
cached client");
+    assertEquals(2, buildCount.get(), "a rotated token must trigger a fresh 
client build");
+  }
+
+  @Test
+  void testResolveSessionMetadataMapsAuthSpecificFailureToPermissionDenied() {
+    CatalogConnectorContext ctx =
+        mockContextWithConfig(
+            ImmutableMap.of(
+                GravitinoAuthProvider.FORWARD_SESSION_USER_KEY, "true",
+                GravitinoAuthProvider.AUTH_TYPE_KEY, "oauth2"));
+    GravitinoConnector connector =
+        newConnectorWithAuthClient(
+            ctx,
+            session -> {
+              throw new IllegalArgumentException("No forwarded user token 
found");
+            });
+
+    TrinoException ex =
+        assertThrows(
+            TrinoException.class,
+            () -> connector.resolveSessionMetadata(mockSession("alice", 
"token-a")));
+    assertEquals(PERMISSION_DENIED.toErrorCode(), ex.getErrorCode());
+  }
+
+  @Test
+  void testResolveSessionMetadataPreservesTrinoExceptionErrorCode() {
+    CatalogConnectorContext ctx =
+        mockContextWithConfig(
+            ImmutableMap.of(
+                GravitinoAuthProvider.FORWARD_SESSION_USER_KEY, "true",
+                GravitinoAuthProvider.AUTH_TYPE_KEY, "oauth2"));
+    GravitinoConnector connector =
+        newConnectorWithAuthClient(
+            ctx,
+            session -> {
+              throw new TrinoException(
+                  GravitinoErrorCode.GRAVITINO_ILLEGAL_ARGUMENT, "No forwarded 
user token found");
+            });
+
+    TrinoException ex =
+        assertThrows(
+            TrinoException.class,
+            () -> connector.resolveSessionMetadata(mockSession("alice", 
"token-a")));
+    assertEquals(GravitinoErrorCode.GRAVITINO_ILLEGAL_ARGUMENT.toErrorCode(), 
ex.getErrorCode());
+  }
+
+  @Test
+  void testResolveSessionMetadataMapsUnexpectedFailureToGenericInternalError() 
{
+    CatalogConnectorContext ctx =
+        mockContextWithConfig(
+            ImmutableMap.of(
+                GravitinoAuthProvider.FORWARD_SESSION_USER_KEY, "true",
+                GravitinoAuthProvider.AUTH_TYPE_KEY, "oauth2"));
+    GravitinoConnector connector =
+        newConnectorWithAuthClient(
+            ctx,
+            session -> {
+              throw new RuntimeException("connection refused");
+            });
+
+    TrinoException ex =
+        assertThrows(
+            TrinoException.class,
+            () -> connector.resolveSessionMetadata(mockSession("alice", 
"token-a")));
+    assertEquals(GENERIC_INTERNAL_ERROR.toErrorCode(), ex.getErrorCode());
+  }
+
+  /**
+   * Creates a {@link GravitinoConnector} whose {@code buildAuthClient} is 
overridden with the given
+   * function, so tests can control what building the per-user client does 
without mocking the
+   * static {@link GravitinoAuthProvider#buildForSession}.
+   */
+  private static GravitinoConnector newConnectorWithAuthClient(
+      CatalogConnectorContext ctx,
+      Function<ConnectorSession, GravitinoAdminClient> authClientBuilder) {
+    return new GravitinoConnector(ctx) {
+      @Override
+      GravitinoAdminClient buildAuthClient(ConnectorSession session) {
+        return authClientBuilder.apply(session);
+      }
+    };
+  }
+
+  private static GravitinoAdminClient mockAdminClient(
+      GravitinoMetalake metalake, AtomicInteger buildCount) {
+    buildCount.incrementAndGet();
+    GravitinoAdminClient client = mock(GravitinoAdminClient.class);
+    when(client.loadMetalake(any())).thenReturn(metalake);
+    return client;
+  }
+
+  private static ConnectorSession mockSession(String user, String token) {
+    ConnectorIdentity identity = mock(ConnectorIdentity.class);
+    when(identity.getExtraCredentials()).thenReturn(ImmutableMap.of("token", 
token));
+    ConnectorSession session = mock(ConnectorSession.class);
+    when(session.getUser()).thenReturn(user);
+    when(session.getIdentity()).thenReturn(identity);
+    return session;
+  }
+
   private static CatalogConnectorContext mockContextWithConfig(
       ImmutableMap<String, String> extraConfig) {
     GravitinoCatalog mockCatalog = mock(GravitinoCatalog.class);
diff --git 
a/trino-connector/trino-connector/src/test/java/org/apache/gravitino/trino/connector/catalog/iceberg/TestIcebergCatalogPropertyConverter.java
 
b/trino-connector/trino-connector/src/test/java/org/apache/gravitino/trino/connector/catalog/iceberg/TestIcebergCatalogPropertyConverter.java
index b9fda2c4b3..9ef2d72d40 100644
--- 
a/trino-connector/trino-connector/src/test/java/org/apache/gravitino/trino/connector/catalog/iceberg/TestIcebergCatalogPropertyConverter.java
+++ 
b/trino-connector/trino-connector/src/test/java/org/apache/gravitino/trino/connector/catalog/iceberg/TestIcebergCatalogPropertyConverter.java
@@ -57,6 +57,33 @@ public class TestIcebergCatalogPropertyConverter {
         "Missing required property for Hive backend: [uri]");
   }
 
+  @Test
+  public void testRestBackendProperty() {
+    PropertyConverter propertyConverter = new 
IcebergCatalogPropertyConverter();
+    Map<String, String> gravitinoIcebergConfig =
+        ImmutableMap.<String, String>builder()
+            .put("uri", "http://localhost:9001/iceberg";)
+            .put("catalog-backend", "rest")
+            .put("warehouse", "gt_iceberg_rest")
+            .build();
+    Map<String, String> restBackendConfig =
+        propertyConverter.gravitinoToEngineProperties(gravitinoIcebergConfig);
+
+    Assertions.assertEquals(restBackendConfig.get("iceberg.catalog.type"), 
"rest");
+    Assertions.assertEquals(
+        restBackendConfig.get("iceberg.rest-catalog.uri"), 
"http://localhost:9001/iceberg";);
+    Assertions.assertEquals(
+        restBackendConfig.get("iceberg.rest-catalog.warehouse"), 
"gt_iceberg_rest");
+
+    Map<String, String> wrongMap = Maps.newHashMap(gravitinoIcebergConfig);
+    wrongMap.remove("uri");
+
+    Assertions.assertThrows(
+        TrinoException.class,
+        () -> propertyConverter.gravitinoToEngineProperties(wrongMap),
+        "Missing required property for Rest backend: [uri]");
+  }
+
   @Test
   public void testJDBCBackendProperty() {
     PropertyConverter propertyConverter = new 
IcebergCatalogPropertyConverter();
diff --git 
a/trino-connector/trino-connector/src/test/java/org/apache/gravitino/trino/connector/security/TestGravitinoAuthProvider.java
 
b/trino-connector/trino-connector/src/test/java/org/apache/gravitino/trino/connector/security/TestGravitinoAuthProvider.java
index 131eee6af2..03ede70a6a 100644
--- 
a/trino-connector/trino-connector/src/test/java/org/apache/gravitino/trino/connector/security/TestGravitinoAuthProvider.java
+++ 
b/trino-connector/trino-connector/src/test/java/org/apache/gravitino/trino/connector/security/TestGravitinoAuthProvider.java
@@ -25,7 +25,9 @@ import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.when;
 
 import com.google.common.collect.ImmutableMap;
+import io.trino.spi.TrinoException;
 import io.trino.spi.connector.ConnectorSession;
+import io.trino.spi.security.ConnectorIdentity;
 import java.io.File;
 import java.io.IOException;
 import java.nio.file.Files;
@@ -259,6 +261,63 @@ public class TestGravitinoAuthProvider {
         () -> GravitinoAuthProvider.buildForSession(config, session));
   }
 
+  @Test
+  public void testBuildForSessionOAuth2() {
+    GravitinoConfig config =
+        buildConfig(
+            ImmutableMap.of(
+                GravitinoAuthProvider.AUTH_TYPE_KEY, "oauth2",
+                GravitinoAuthProvider.FORWARD_SESSION_USER_KEY, "true"));
+
+    ConnectorSession session = mock(ConnectorSession.class);
+    when(session.getUser()).thenReturn("alice");
+    when(session.getIdentity())
+        .thenReturn(
+            ConnectorIdentity.forUser("alice")
+                .withExtraCredentials(ImmutableMap.of("token", 
"forwarded-user-jwt"))
+                .build());
+
+    GravitinoAdminClient client = 
GravitinoAuthProvider.buildForSession(config, session);
+    assertNotNull(client);
+  }
+
+  @Test
+  public void testBuildForSessionOAuth2WithCustomCredentialKey() {
+    GravitinoConfig config =
+        buildConfig(
+            ImmutableMap.of(
+                GravitinoAuthProvider.AUTH_TYPE_KEY, "oauth2",
+                GravitinoAuthProvider.FORWARD_SESSION_USER_KEY, "true",
+                GravitinoAuthProvider.USER_TOKEN_CREDENTIAL_KEY, 
"access_token"));
+
+    ConnectorSession session = mock(ConnectorSession.class);
+    when(session.getUser()).thenReturn("alice");
+    when(session.getIdentity())
+        .thenReturn(
+            ConnectorIdentity.forUser("alice")
+                .withExtraCredentials(ImmutableMap.of("access_token", 
"alice-jwt"))
+                .build());
+
+    GravitinoAdminClient client = 
GravitinoAuthProvider.buildForSession(config, session);
+    assertNotNull(client);
+  }
+
+  @Test
+  public void testBuildForSessionOAuth2ThrowsWhenTokenMissing() {
+    GravitinoConfig config =
+        buildConfig(
+            ImmutableMap.of(
+                GravitinoAuthProvider.AUTH_TYPE_KEY, "oauth2",
+                GravitinoAuthProvider.FORWARD_SESSION_USER_KEY, "true"));
+
+    ConnectorSession session = mock(ConnectorSession.class);
+    when(session.getUser()).thenReturn("alice");
+    
when(session.getIdentity()).thenReturn(ConnectorIdentity.forUser("alice").build());
+
+    assertThrows(
+        TrinoException.class, () -> 
GravitinoAuthProvider.buildForSession(config, session));
+  }
+
   private GravitinoConfig buildConfig(ImmutableMap<String, String> authConfig) 
{
     ImmutableMap.Builder<String, String> builder =
         ImmutableMap.<String, String>builder()

Reply via email to