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

okumin pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/hive.git


The following commit(s) were added to refs/heads/master by this push:
     new 3509fbe78e4 HIVE-30199: Fix inverted Duration.between in 
TokenIntrospectionAuthenticator cache expiry (#6892)
3509fbe78e4 is described below

commit 3509fbe78e4967a769de570bb4744342cccb7c4b
Author: Raghav Aggarwal <[email protected]>
AuthorDate: Mon Oct 19 16:53:09 2026 +0530

    HIVE-30199: Fix inverted Duration.between in 
TokenIntrospectionAuthenticator cache expiry (#6892)
---
 .../oauth2/TokenIntrospectionAuthenticator.java    |  12 +-
 .../metastore/auth/oauth2/TestTokenExpiry.java     | 140 +++++++++++++++++++++
 2 files changed, 147 insertions(+), 5 deletions(-)

diff --git 
a/standalone-metastore/metastore-server/src/main/java/org/apache/hadoop/hive/metastore/auth/oauth2/TokenIntrospectionAuthenticator.java
 
b/standalone-metastore/metastore-server/src/main/java/org/apache/hadoop/hive/metastore/auth/oauth2/TokenIntrospectionAuthenticator.java
index 5c12f80750a..95b1563bc6d 100644
--- 
a/standalone-metastore/metastore-server/src/main/java/org/apache/hadoop/hive/metastore/auth/oauth2/TokenIntrospectionAuthenticator.java
+++ 
b/standalone-metastore/metastore-server/src/main/java/org/apache/hadoop/hive/metastore/auth/oauth2/TokenIntrospectionAuthenticator.java
@@ -22,6 +22,7 @@
 import com.github.benmanes.caffeine.cache.Cache;
 import com.github.benmanes.caffeine.cache.Caffeine;
 import com.github.benmanes.caffeine.cache.Expiry;
+import com.google.common.annotations.VisibleForTesting;
 import com.nimbusds.oauth2.sdk.ParseException;
 import com.nimbusds.oauth2.sdk.TokenIntrospectionRequest;
 import com.nimbusds.oauth2.sdk.TokenIntrospectionResponse;
@@ -55,20 +56,21 @@ private UncheckedException(HttpAuthenticationException 
cause) {
     }
   }
 
-  private record TokenExpiry(Duration maxExpiration, Clock clock) implements
+  @VisibleForTesting
+  record TokenExpiry(Duration maxExpiration, Clock clock) implements
       Expiry<String, TokenIntrospectionSuccessResponse> {
     @Override
     public long expireAfterCreate(@NonNull String key, @NonNull 
TokenIntrospectionSuccessResponse value,
         long currentTime) {
-      final var expiresIn = 
Duration.between(value.getExpirationTime().toInstant(), clock.instant());
-      return Math.min(expiresIn.toNanos(), maxExpiration.toNanos());
+      final var expiresIn = Duration.between(clock.instant(), 
value.getExpirationTime().toInstant());
+      return Math.clamp(expiresIn.toNanos(), 0L, maxExpiration.toNanos());
     }
 
     @Override
     public long expireAfterUpdate(@NonNull String key, @NonNull 
TokenIntrospectionSuccessResponse value,
         long currentTime, @NonNegative long currentDuration) {
-      final var expiresIn = 
Duration.between(value.getExpirationTime().toInstant(), clock.instant());
-      return Math.min(expiresIn.toNanos(), maxExpiration.toNanos());
+      final var expiresIn = Duration.between(clock.instant(), 
value.getExpirationTime().toInstant());
+      return Math.clamp(expiresIn.toNanos(), 0L, maxExpiration.toNanos());
     }
 
     @Override
diff --git 
a/standalone-metastore/metastore-server/src/test/java/org/apache/hadoop/hive/metastore/auth/oauth2/TestTokenExpiry.java
 
b/standalone-metastore/metastore-server/src/test/java/org/apache/hadoop/hive/metastore/auth/oauth2/TestTokenExpiry.java
new file mode 100644
index 00000000000..e09c982d6bc
--- /dev/null
+++ 
b/standalone-metastore/metastore-server/src/test/java/org/apache/hadoop/hive/metastore/auth/oauth2/TestTokenExpiry.java
@@ -0,0 +1,140 @@
+/*
+ * 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.hadoop.hive.metastore.auth.oauth2;
+
+import com.github.benmanes.caffeine.cache.Cache;
+import com.github.benmanes.caffeine.cache.Caffeine;
+import com.nimbusds.oauth2.sdk.TokenIntrospectionSuccessResponse;
+import java.time.Clock;
+import java.time.Duration;
+import java.time.Instant;
+import java.time.ZoneOffset;
+import java.util.Date;
+import java.util.concurrent.atomic.AtomicBoolean;
+import org.apache.hadoop.hive.metastore.annotation.MetastoreUnitTest;
+import 
org.apache.hadoop.hive.metastore.auth.oauth2.TokenIntrospectionAuthenticator.TokenExpiry;
+import org.junit.Assert;
+import org.junit.Test;
+import org.junit.experimental.categories.Category;
+
+@Category(MetastoreUnitTest.class)
+public class TestTokenExpiry {
+  private static final Instant NOW = Instant.parse("2026-01-01T00:00:00Z");
+  private static final Clock CLOCK = Clock.fixed(NOW, ZoneOffset.UTC);
+  private static final Duration MAX_EXPIRATION = Duration.ofMinutes(10);
+  private static final TokenExpiry EXPIRY = new TokenExpiry(MAX_EXPIRATION, 
CLOCK);
+
+  private static TokenIntrospectionSuccessResponse createResponse(Instant 
expirationTime) {
+    return new TokenIntrospectionSuccessResponse.Builder(true)
+        .expirationTime(Date.from(expirationTime))
+        .build();
+  }
+
+  @Test
+  public void testExpireAfterCreateWithFutureToken() {
+    TokenIntrospectionSuccessResponse response = 
createResponse(NOW.plus(Duration.ofMinutes(5)));
+    long nanos = EXPIRY.expireAfterCreate("token1", response, 0L);
+
+    Assert.assertTrue("Expiry duration for valid future token must be 
positive", nanos > 0);
+    Assert.assertEquals(Duration.ofMinutes(5).toNanos(), nanos);
+  }
+
+  @Test
+  public void testCacheRetainsValidToken() {
+    Cache<String, TokenIntrospectionSuccessResponse> cache =
+        Caffeine.newBuilder().expireAfter(EXPIRY).build();
+
+    TokenIntrospectionSuccessResponse response = 
createResponse(NOW.plus(Duration.ofMinutes(5)));
+    cache.put("token1", response);
+
+    Assert.assertSame(
+        "Valid token must be cached and not expired immediately",
+        response,
+        cache.getIfPresent("token1"));
+  }
+
+  @Test
+  public void testCacheHitBypassesLoader() {
+    Cache<String, TokenIntrospectionSuccessResponse> cache =
+        Caffeine.newBuilder().expireAfter(EXPIRY).build();
+
+    TokenIntrospectionSuccessResponse response = 
createResponse(NOW.plus(Duration.ofMinutes(5)));
+    cache.put("token1", response);
+
+    AtomicBoolean loaderCalled = new AtomicBoolean(false);
+    TokenIntrospectionSuccessResponse cached =
+        cache.get(
+            "token1",
+            k -> {
+              loaderCalled.set(true);
+              return createResponse(NOW.plus(Duration.ofMinutes(10)));
+            });
+
+    Assert.assertSame("Cache hit must return existing instance", response, 
cached);
+    Assert.assertFalse("Cache loader must not be invoked on hit", 
loaderCalled.get());
+  }
+
+  @Test
+  public void testExpireAfterCreateCapsAtMaxExpiration() {
+    TokenIntrospectionSuccessResponse response = 
createResponse(NOW.plus(Duration.ofHours(1)));
+    long nanos = EXPIRY.expireAfterCreate("token1", response, 0L);
+
+    Assert.assertEquals("Expiry must be capped at maxExpiration", 
MAX_EXPIRATION.toNanos(), nanos);
+  }
+
+  @Test
+  public void testExpireAfterCreateWithExpiredToken() {
+    TokenIntrospectionSuccessResponse response = 
createResponse(NOW.minus(Duration.ofMinutes(1)));
+    long nanos = EXPIRY.expireAfterCreate("token1", response, 0L);
+
+    Assert.assertEquals("Expired token must return 0 nanos duration", 0L, 
nanos);
+  }
+
+  @Test
+  public void testCacheEvictsExpiredTokenImmediately() {
+    Cache<String, TokenIntrospectionSuccessResponse> cache =
+        Caffeine.newBuilder().expireAfter(EXPIRY).build();
+
+    TokenIntrospectionSuccessResponse response = 
createResponse(NOW.minus(Duration.ofMinutes(1)));
+    cache.put("expiredToken", response);
+
+    Assert.assertNull(
+        "Expired token must not be retained in cache", 
cache.getIfPresent("expiredToken"));
+  }
+
+  @Test
+  public void testExpireAfterUpdate() {
+    TokenIntrospectionSuccessResponse response = 
createResponse(NOW.plus(Duration.ofMinutes(3)));
+    long nanos = EXPIRY.expireAfterUpdate("token1", response, 0L, 
Duration.ofMinutes(5).toNanos());
+
+    Assert.assertTrue("Expiry duration on update must be positive", nanos > 0);
+    Assert.assertEquals(Duration.ofMinutes(3).toNanos(), nanos);
+  }
+
+  @Test
+  public void testExpireAfterReadPreservesDuration() {
+    TokenIntrospectionSuccessResponse response = 
createResponse(NOW.plus(Duration.ofMinutes(5)));
+    long currentDuration = Duration.ofMinutes(4).toNanos();
+    long nanos = EXPIRY.expireAfterRead("token1", response, 0L, 
currentDuration);
+
+    Assert.assertEquals(
+        "expireAfterRead must preserve the current duration", currentDuration, 
nanos);
+  }
+}

Reply via email to