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

adutra 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 f2947edf3 JWTBroker: fix refresh token logic (#1242)
f2947edf3 is described below

commit f2947edf3c2a165db12d54e7acfefe5f9de6e6a1
Author: Alexandre Dutra <[email protected]>
AuthorDate: Sun Mar 23 00:01:34 2025 +0100

    JWTBroker: fix refresh token logic (#1242)
---
 .../it/QuarkusApplicationIntegrationTest.java      | 100 ++++++++++++++++++++-
 .../org/apache/polaris/service/auth/JWTBroker.java |  10 ++-
 2 files changed, 107 insertions(+), 3 deletions(-)

diff --git 
a/quarkus/service/src/test/java/org/apache/polaris/service/quarkus/it/QuarkusApplicationIntegrationTest.java
 
b/quarkus/service/src/test/java/org/apache/polaris/service/quarkus/it/QuarkusApplicationIntegrationTest.java
index 8aaac080e..ba1395ac8 100644
--- 
a/quarkus/service/src/test/java/org/apache/polaris/service/quarkus/it/QuarkusApplicationIntegrationTest.java
+++ 
b/quarkus/service/src/test/java/org/apache/polaris/service/quarkus/it/QuarkusApplicationIntegrationTest.java
@@ -19,19 +19,26 @@
 package org.apache.polaris.service.quarkus.it;
 
 import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
 
 import com.auth0.jwt.JWT;
 import com.auth0.jwt.algorithms.Algorithm;
 import io.quarkus.test.junit.QuarkusTest;
 import io.quarkus.test.junit.QuarkusTestProfile;
 import io.quarkus.test.junit.TestProfile;
+import io.smallrye.common.annotation.Identifier;
+import jakarta.inject.Inject;
 import java.io.IOException;
 import java.time.Instant;
 import java.util.Map;
+import org.apache.iceberg.exceptions.NotAuthorizedException;
+import org.apache.iceberg.rest.ErrorHandlers;
 import org.apache.iceberg.rest.HTTPClient;
 import org.apache.iceberg.rest.RESTClient;
 import org.apache.iceberg.rest.auth.AuthConfig;
 import org.apache.iceberg.rest.auth.OAuth2Util;
+import org.apache.iceberg.rest.responses.OAuthTokenResponse;
+import org.apache.polaris.service.auth.TokenBrokerFactory;
 import org.apache.polaris.service.it.env.ClientCredentials;
 import org.apache.polaris.service.it.env.PolarisApiEndpoints;
 import org.apache.polaris.service.it.test.PolarisApplicationIntegrationTest;
@@ -53,8 +60,12 @@ public class QuarkusApplicationIntegrationTest extends 
PolarisApplicationIntegra
     }
   }
 
+  @Inject
+  @Identifier("rsa-key-pair")
+  TokenBrokerFactory tokenBrokerFactory;
+
   @Test
-  public void testIcebergRestApiRefreshToken(
+  public void testIcebergRestApiRefreshExpiredToken(
       PolarisApiEndpoints endpoints, ClientCredentials clientCredentials) 
throws IOException {
     String path = endpoints.catalogApiEndpoint() + "/v1/oauth/tokens";
     try (RESTClient client =
@@ -82,4 +93,91 @@ public class QuarkusApplicationIntegrationTest extends 
PolarisApplicationIntegra
       
assertThat(JWT.decode(session.token()).getExpiresAtAsInstant()).isAfter(Instant.EPOCH);
     }
   }
+
+  @Test
+  public void testIcebergRestApiRefreshValidToken(
+      PolarisApiEndpoints endpoints, ClientCredentials clientCredentials) 
throws IOException {
+    String path = endpoints.catalogApiEndpoint() + "/v1/oauth/tokens";
+    try (RESTClient client =
+        HTTPClient.builder(Map.of())
+            .withHeader(endpoints.realmHeaderName(), endpoints.realmId())
+            .uri(path)
+            .build()) {
+      var response =
+          client.postForm(
+              path,
+              Map.of(
+                  "grant_type",
+                  "client_credentials",
+                  "scope",
+                  "PRINCIPAL_ROLE:ALL",
+                  "client_id",
+                  clientCredentials.clientId(),
+                  "client_secret",
+                  clientCredentials.clientSecret()),
+              OAuthTokenResponse.class,
+              Map.of(),
+              ErrorHandlers.oauthErrorHandler());
+      String token = response.token();
+      var authConfig =
+          AuthConfig.builder()
+              .credential(clientCredentials.clientId() + ":" + 
clientCredentials.clientSecret())
+              .scope("PRINCIPAL_ROLE:ALL")
+              .oauth2ServerUri(path)
+              .token(token)
+              .build();
+      var parentSession = new OAuth2Util.AuthSession(Map.of(), authConfig);
+      var session = OAuth2Util.AuthSession.fromAccessToken(client, null, 
token, 0L, parentSession);
+      session.refresh(client);
+      assertThat(session.token()).isNotEqualTo(token);
+      
assertThat(JWT.decode(session.token()).getExpiresAtAsInstant()).isAfter(Instant.now());
+    }
+  }
+
+  @Test
+  public void testIcebergRestApiInvalidToken(
+      PolarisApiEndpoints endpoints, ClientCredentials clientCredentials) 
throws IOException {
+    String path = endpoints.catalogApiEndpoint() + "/v1/oauth/tokens";
+    try (RESTClient client =
+        HTTPClient.builder(Map.of())
+            .withHeader(endpoints.realmHeaderName(), endpoints.realmId())
+            .uri(path)
+            .build()) {
+      var response =
+          client.postForm(
+              path,
+              Map.of(
+                  "grant_type",
+                  "client_credentials",
+                  "scope",
+                  "PRINCIPAL_ROLE:ALL",
+                  "client_id",
+                  clientCredentials.clientId(),
+                  "client_secret",
+                  clientCredentials.clientSecret()),
+              OAuthTokenResponse.class,
+              Map.of(),
+              ErrorHandlers.oauthErrorHandler());
+      String token = response.token();
+      // mimics OAUth2Util.AuthSession refreshing the token
+      assertThatThrownBy(
+              () ->
+                  client.postForm(
+                      path,
+                      Map.of(
+                          "grant_type",
+                          "urn:ietf:params:oauth:grant-type:token-exchange",
+                          "scope",
+                          "PRINCIPAL_ROLE:ALL",
+                          "subject_token",
+                          "invalid",
+                          "subject_token_type",
+                          "urn:ietf:params:oauth:token-type:access_token"),
+                      OAuthTokenResponse.class,
+                      Map.of("Authorization", "Bearer " + token),
+                      ErrorHandlers.oauthErrorHandler()))
+          .isInstanceOf(NotAuthorizedException.class)
+          .hasMessageContaining("invalid_client");
+    }
+  }
 }
diff --git 
a/service/common/src/main/java/org/apache/polaris/service/auth/JWTBroker.java 
b/service/common/src/main/java/org/apache/polaris/service/auth/JWTBroker.java
index 43e4804aa..dafe0732d 100644
--- 
a/service/common/src/main/java/org/apache/polaris/service/auth/JWTBroker.java
+++ 
b/service/common/src/main/java/org/apache/polaris/service/auth/JWTBroker.java
@@ -35,6 +35,7 @@ import org.apache.polaris.core.entity.PolarisEntityType;
 import org.apache.polaris.core.entity.PrincipalEntity;
 import org.apache.polaris.core.persistence.PolarisMetaStoreManager;
 import org.apache.polaris.core.persistence.dao.entity.EntityResult;
+import org.apache.polaris.service.auth.OAuthTokenErrorResponse.Error;
 import org.apache.polaris.service.types.TokenType;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -100,7 +101,7 @@ public abstract class JWTBroker implements TokenBroker {
       String grantType,
       String scope,
       TokenType requestedTokenType) {
-    if (!TokenType.ACCESS_TOKEN.equals(requestedTokenType)) {
+    if (requestedTokenType != null && 
!TokenType.ACCESS_TOKEN.equals(requestedTokenType)) {
       return new TokenResponse(OAuthTokenErrorResponse.Error.invalid_request);
     }
     if (!TokenType.ACCESS_TOKEN.equals(subjectTokenType)) {
@@ -109,7 +110,12 @@ public abstract class JWTBroker implements TokenBroker {
     if (StringUtils.isBlank(subjectToken)) {
       return new TokenResponse(OAuthTokenErrorResponse.Error.invalid_request);
     }
-    DecodedToken decodedToken = verify(subjectToken);
+    DecodedToken decodedToken;
+    try {
+      decodedToken = verify(subjectToken);
+    } catch (NotAuthorizedException e) {
+      return new TokenResponse(Error.invalid_client);
+    }
     EntityResult principalLookup =
         metaStoreManager.loadEntity(
             CallContext.getCurrentContext().getPolarisCallContext(),

Reply via email to