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

sodonnell pushed a commit to branch HDDS-13323-sts
in repository https://gitbox.apache.org/repos/asf/ozone.git


The following commit(s) were added to refs/heads/HDDS-13323-sts by this push:
     new 36852379d5c HDDS-13997. [STS] Plumbing for passing STS token through 
S3 api processing (#9372)
36852379d5c is described below

commit 36852379d5c4adf7b4022efbc3c5d153528acb7b
Author: fmorg-git <[email protected]>
AuthorDate: Fri Dec 5 02:53:56 2025 -0800

    HDDS-13997. [STS] Plumbing for passing STS token through S3 api processing 
(#9372)
---
 .../apache/hadoop/ozone/om/protocol/S3Auth.java    |  10 +
 ...OzoneManagerProtocolClientSideTranslatorPB.java |  12 +-
 .../apache/hadoop/ozone/om/OmMetadataReader.java   |   6 +-
 .../org/apache/hadoop/ozone/om/OzoneManager.java   |  65 ++++-
 .../hadoop/ozone/om/request/OMClientRequest.java   |  31 +-
 .../request/s3/security/S3AssumeRoleRequest.java   |   8 +-
 ...OzoneManagerProtocolServerSideTranslatorPB.java |   1 +
 .../hadoop/ozone/security/S3SecurityUtil.java      |  35 +++
 .../hadoop/ozone/security/STSSecurityUtil.java     | 153 ++++++++++
 .../ozone/security/STSTokenSecretManager.java      |  26 +-
 .../hadoop/ozone/om/TestOzoneManagerS3Auth.java    | 123 ++++++++
 .../request/TestOMClientRequestWithUserInfo.java   | 137 +++++++++
 .../hadoop/ozone/security/TestSTSSecurityUtil.java | 318 +++++++++++++++++++++
 .../ozone/security/TestSTSTokenSecretManager.java  |  16 +-
 14 files changed, 917 insertions(+), 24 deletions(-)

diff --git 
a/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/protocol/S3Auth.java
 
b/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/protocol/S3Auth.java
index 84acade8f9a..fa023dfc811 100644
--- 
a/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/protocol/S3Auth.java
+++ 
b/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/protocol/S3Auth.java
@@ -27,6 +27,8 @@ public class S3Auth {
   public static final String S3_AUTH_CHECK = "ozone.s3.auth.check";
   // User principal to be used for KMS encryption and decryption
   private String userPrincipal;
+  // Optional STS session token when using temporary credentials
+  private String sessionToken;
 
   public S3Auth(final String stringToSign,
                 final String signature,
@@ -57,4 +59,12 @@ public String getUserPrincipal() {
   public void setUserPrincipal(String userPrincipal) {
     this.userPrincipal = userPrincipal;
   }
+
+  public String getSessionToken() {
+    return sessionToken;
+  }
+
+  public void setSessionToken(String sessionToken) {
+    this.sessionToken = sessionToken;
+  }
 }
diff --git 
a/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/protocolPB/OzoneManagerProtocolClientSideTranslatorPB.java
 
b/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/protocolPB/OzoneManagerProtocolClientSideTranslatorPB.java
index c3c173a8cae..36adbe7b37f 100644
--- 
a/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/protocolPB/OzoneManagerProtocolClientSideTranslatorPB.java
+++ 
b/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/protocolPB/OzoneManagerProtocolClientSideTranslatorPB.java
@@ -314,15 +314,21 @@ private OMResponse submitRequest(OMRequest omRequest)
     OMRequest.Builder  builder = OMRequest.newBuilder(omRequest);
     // Insert S3 Authentication information for each request.
     if (getThreadLocalS3Auth() != null) {
-      builder.setS3Authentication(
+      final S3Authentication.Builder s3AuthBuilder =
           S3Authentication.newBuilder()
               .setSignature(
                   threadLocalS3Auth.get().getSignature())
               .setStringToSign(
                   threadLocalS3Auth.get().getStringTosSign())
               .setAccessId(
-                  threadLocalS3Auth.get().getAccessID())
-              .build());
+                  threadLocalS3Auth.get().getAccessID());
+
+      // Include STS session token if present so OM can validate it
+      if (threadLocalS3Auth.get().getSessionToken() != null) {
+        
s3AuthBuilder.setSessionToken(threadLocalS3Auth.get().getSessionToken());
+      }
+
+      builder.setS3Authentication(s3AuthBuilder.build());
     }
     if (s3AuthCheck && getThreadLocalS3Auth() == null) {
       throw new IllegalArgumentException("S3 Auth expected to " +
diff --git 
a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/OmMetadataReader.java
 
b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/OmMetadataReader.java
index c413c96956f..2fac369e3a2 100644
--- 
a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/OmMetadataReader.java
+++ 
b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/OmMetadataReader.java
@@ -488,8 +488,9 @@ void checkAcls(ResourceType resType, StoreType store,
       throws IOException {
     UserGroupInformation user;
     if (getS3Auth() != null) {
+      final String effectiveAccessId = 
OzoneManager.getS3AuthEffectiveAccessId();
       String principal =
-          OzoneAclUtils.accessIdToUserPrincipal(getS3Auth().getAccessId());
+          OzoneAclUtils.accessIdToUserPrincipal(effectiveAccessId);
       user = UserGroupInformation.createRemoteUser(principal);
     } else {
       user = ProtobufRpcEngine.Server.getRemoteUser();
@@ -523,8 +524,9 @@ void checkAcls(ResourceType resType, StoreType store,
       throws IOException {
     UserGroupInformation user;
     if (getS3Auth() != null) {
+      final String effectiveAccessId = 
OzoneManager.getS3AuthEffectiveAccessId();
       String principal =
-          OzoneAclUtils.accessIdToUserPrincipal(getS3Auth().getAccessId());
+          OzoneAclUtils.accessIdToUserPrincipal(effectiveAccessId);
       user = UserGroupInformation.createRemoteUser(principal);
     } else {
       user = ProtobufRpcEngine.Server.getRemoteUser();
diff --git 
a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/OzoneManager.java
 
b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/OzoneManager.java
index 5fe753871a1..e6c5f916cc1 100644
--- 
a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/OzoneManager.java
+++ 
b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/OzoneManager.java
@@ -307,6 +307,7 @@
 import org.apache.hadoop.ozone.security.OMCertificateClient;
 import org.apache.hadoop.ozone.security.OzoneDelegationTokenSecretManager;
 import org.apache.hadoop.ozone.security.OzoneTokenIdentifier;
+import org.apache.hadoop.ozone.security.STSTokenIdentifier;
 import org.apache.hadoop.ozone.security.STSTokenSecretManager;
 import org.apache.hadoop.ozone.security.acl.IAccessAuthorizer;
 import org.apache.hadoop.ozone.security.acl.IAccessAuthorizer.ACLIdentityType;
@@ -378,6 +379,9 @@ public final class OzoneManager extends 
ServiceRuntimeInfoImpl
   private static final ThreadLocal<S3Authentication> S3_AUTH =
       new ThreadLocal<>();
 
+  // STS token (if present)
+  private static final ThreadLocal<STSTokenIdentifier> STS_TOKEN = new 
ThreadLocal<>();
+
   private static boolean securityEnabled = false;
 
   private final ReconfigurationHandler reconfigurationHandler;
@@ -858,6 +862,47 @@ public static S3Authentication getS3Auth() {
     return S3_AUTH.get();
   }
 
+  /**
+   * Set the STS token identifier for the current RPC handler thread.
+   */
+  public static void setStsTokenIdentifier(STSTokenIdentifier val) {
+    STS_TOKEN.set(val);
+  }
+
+  /**
+   * Get the STS token identifier for the current RPC handler thread.
+   */
+  public static STSTokenIdentifier getStsTokenIdentifier() {
+    return STS_TOKEN.get();
+  }
+
+  /**
+   * Returns the effective accessId for the current request.  If STS temporary 
credentials are being used,
+   * the access key id will be the original access key id (i.e. the creator of 
the token).
+   */
+  public static String getS3AuthEffectiveAccessId() throws OMException {
+    final S3Authentication s3Auth = getS3Auth();
+    if (s3Auth == null) {
+      return null;
+    }
+
+    // If session token is present, try to resolve originalAccessKeyId from 
token
+    if (s3Auth.hasSessionToken() && !s3Auth.getSessionToken().isEmpty()) {
+      final STSTokenIdentifier stsTokenIdentifier = getStsTokenIdentifier();
+      if (stsTokenIdentifier == null) {
+        throw new OMException(
+            "OMClientRequest has session token but no token identifier in 
OzoneManager", INVALID_REQUEST);
+      }
+      final String originalAccessKeyId = 
stsTokenIdentifier.getOriginalAccessKeyId();
+      if (originalAccessKeyId != null && !originalAccessKeyId.isEmpty()) {
+        return originalAccessKeyId;
+      } else {
+        throw new OMException("Invalid STS Token format - could not find 
originalAccessKeyId", INVALID_REQUEST);
+      }
+    }
+    return s3Auth.getAccessId();
+  }
+
   /** Returns the ThreadName prefix for the current OM. */
   public String getThreadNamePrefix() {
     return threadPrefix;
@@ -1269,6 +1314,15 @@ private void stopSecretManager() {
     }
   }
 
+  /**
+   * Get the secret key client for this OzoneManager.
+   *
+   * @return the secret key client
+   */
+  public SecretKeyClient getSecretKeyClient() {
+    return secretKeyClient;
+  }
+
   @Override
   public UUID refetchSecretKey() {
     secretKeyClient.refetchSecretKey();
@@ -3836,7 +3890,12 @@ S3VolumeContext getS3VolumeContext(boolean skipChecks) 
throws IOException {
             s3Volume);
       }
     } else {
-      String accessId = s3Auth.getAccessId();
+      // If this S3 request is authenticated with an STS session token, map
+      // the request to the *original* long-lived access ID so that the
+      // temporary credentials inherit that user's ACLs. Otherwise, fall back
+      // to the accessId included directly in the S3Authentication.
+      final String accessId = getS3AuthEffectiveAccessId();
+
       // If S3 Multi-Tenancy is not enabled, all S3 requests will be redirected
       // to the default s3v for compatibility
       final Optional<String> optionalTenantId = isS3MultiTenancyEnabled() ?
@@ -4621,8 +4680,8 @@ public ResolvedBucket resolveBucketLink(Pair<String, 
String> requested,
     if (aclEnabled) {
       UserGroupInformation ugi = getRemoteUser();
       if (getS3Auth() != null) {
-        ugi = UserGroupInformation.createRemoteUser(
-            OzoneAclUtils.accessIdToUserPrincipal(getS3Auth().getAccessId()));
+        final String principal = 
OzoneAclUtils.accessIdToUserPrincipal(getS3AuthEffectiveAccessId());
+        ugi = UserGroupInformation.createRemoteUser(principal);
       }
       InetAddress remoteIp = Server.getRemoteIp();
       resolved = resolveBucketLink(requested, new HashSet<>(),
diff --git 
a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/request/OMClientRequest.java
 
b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/request/OMClientRequest.java
index e7689a90b81..b527b20ea65 100644
--- 
a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/request/OMClientRequest.java
+++ 
b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/request/OMClientRequest.java
@@ -48,6 +48,7 @@
 import org.apache.hadoop.ozone.om.lock.OMLockDetails;
 import org.apache.hadoop.ozone.om.protocolPB.grpc.GrpcClientConstants;
 import org.apache.hadoop.ozone.om.ratis.utils.OzoneManagerRatisUtils;
+import org.apache.hadoop.ozone.om.request.s3.security.S3AssumeRoleRequest;
 import org.apache.hadoop.ozone.om.response.OMClientResponse;
 import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos;
 import 
org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.LayoutVersion;
@@ -168,11 +169,33 @@ public OzoneManagerProtocolProtos.UserInfo getUserInfo() 
throws IOException {
     OzoneManagerProtocolProtos.UserInfo.Builder userInfo =
         OzoneManagerProtocolProtos.UserInfo.newBuilder();
 
-    // If S3 Authentication is set, determine user based on access ID.
+    // If S3 Authentication is set, determine user based on STS token first,
+    // falling back to accessId if session token not present.
     if (omRequest.hasS3Authentication()) {
-      String principal = OzoneAclUtils.accessIdToUserPrincipal(
-          omRequest.getS3Authentication().getAccessId());
-      userInfo.setUserName(principal);
+      final String accessKeyId = omRequest.getS3Authentication().getAccessId();
+      if (accessKeyId.startsWith(S3AssumeRoleRequest.STS_TOKEN_PREFIX) &&
+          !omRequest.getS3Authentication().hasSessionToken()) {
+        throw new IOException("Error with STS token", new 
AuthenticationException(
+            "Missing session token for accessKeyId: " + accessKeyId));
+      }
+      if (omRequest.getS3Authentication().hasSessionToken()) {
+        try {
+          final String originalAccessKeyId = 
OzoneManager.getS3AuthEffectiveAccessId();
+          if (originalAccessKeyId != null && !originalAccessKeyId.isEmpty()) {
+            final String principal = 
OzoneAclUtils.accessIdToUserPrincipal(originalAccessKeyId);
+            userInfo.setUserName(principal);
+          } else {
+            throw new AuthenticationException(
+                "Invalid STS Token - originalAccessKeyId was null or empty: " 
+ originalAccessKeyId);
+          }
+        } catch (Exception e) {
+          throw new IOException("Error with STS Token", e);
+        }
+      } else {
+        String principal = OzoneAclUtils.accessIdToUserPrincipal(
+            omRequest.getS3Authentication().getAccessId());
+        userInfo.setUserName(principal);
+      }
     } else if (user != null) {
       // Added not null checks, as in UT's these values might be null.
       userInfo.setUserName(user.getUserName());
diff --git 
a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/request/s3/security/S3AssumeRoleRequest.java
 
b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/request/s3/security/S3AssumeRoleRequest.java
index 9d092eaba01..b02f78e5643 100644
--- 
a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/request/s3/security/S3AssumeRoleRequest.java
+++ 
b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/request/s3/security/S3AssumeRoleRequest.java
@@ -21,7 +21,9 @@
 import java.io.IOException;
 import java.net.InetAddress;
 import java.security.SecureRandom;
+import java.time.Clock;
 import java.time.Instant;
+import java.time.ZoneOffset;
 import org.apache.commons.lang3.StringUtils;
 import org.apache.hadoop.ipc.ProtobufRpcEngine;
 import org.apache.hadoop.ozone.om.OzoneAclUtils;
@@ -58,7 +60,6 @@ public class S3AssumeRoleRequest extends OMClientRequest {
 
   private static final int MIN_TOKEN_EXPIRATION_SECONDS = 900;    // 15 
minutes in seconds
   private static final int MAX_TOKEN_EXPIRATION_SECONDS = 43200;  // 12 hours 
in seconds
-  private static final String STS_TOKEN_PREFIX = "ASIA";
   private static final int STS_ACCESS_KEY_ID_LENGTH = 20;
   private static final int STS_SECRET_ACCESS_KEY_LENGTH = 40;
   private static final int STS_ROLE_ID_LENGTH = 16;
@@ -70,6 +71,9 @@ public class S3AssumeRoleRequest extends OMClientRequest {
   private static final String CHARS_FOR_SECRET_ACCESS_KEYS = 
CHARS_FOR_ACCESS_KEY_IDS +
       "abcdefghijklmnopqrstuvwxyz/+";
   private static final int CHARS_FOR_SECRET_ACCESS_KEYS_LENGTH = 
CHARS_FOR_SECRET_ACCESS_KEYS.length();
+  private static final Clock CLOCK = Clock.system(ZoneOffset.UTC);
+
+  public static final String STS_TOKEN_PREFIX = "ASIA";
 
   public S3AssumeRoleRequest(OMRequest omRequest) {
     super(omRequest);
@@ -197,7 +201,7 @@ private String generateSessionToken(String targetRoleName, 
OMRequest omRequest,
 
     return ozoneManager.getSTSTokenSecretManager().createSTSTokenString(
         tempAccessKeyId, originalAccessKeyId, roleArn, 
assumeRoleRequest.getDurationSeconds(), secretAccessKey,
-        sessionPolicy);
+        sessionPolicy, CLOCK);
   }
 
   /**
diff --git 
a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/protocolPB/OzoneManagerProtocolServerSideTranslatorPB.java
 
b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/protocolPB/OzoneManagerProtocolServerSideTranslatorPB.java
index 251e81e83ed..de384ef9c4d 100644
--- 
a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/protocolPB/OzoneManagerProtocolServerSideTranslatorPB.java
+++ 
b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/protocolPB/OzoneManagerProtocolServerSideTranslatorPB.java
@@ -174,6 +174,7 @@ private OMResponse internalProcessRequest(OMRequest 
request) throws ServiceExcep
       return ozoneManager.getOmExecutionFlow().submit(request);
     } finally {
       OzoneManager.setS3Auth(null);
+      OzoneManager.setStsTokenIdentifier(null);
     }
   }
 
diff --git 
a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/security/S3SecurityUtil.java
 
b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/security/S3SecurityUtil.java
index cbe0ce414fe..e31f822b2fb 100644
--- 
a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/security/S3SecurityUtil.java
+++ 
b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/security/S3SecurityUtil.java
@@ -21,6 +21,8 @@
 import static 
org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.OMTokenProto.Type.S3AUTHINFO;
 
 import com.google.protobuf.ServiceException;
+import java.time.Clock;
+import java.time.ZoneOffset;
 import org.apache.hadoop.hdds.annotation.InterfaceAudience;
 import org.apache.hadoop.hdds.annotation.InterfaceStability;
 import org.apache.hadoop.io.Text;
@@ -41,6 +43,8 @@
 @InterfaceStability.Evolving
 public final class S3SecurityUtil {
 
+  private static final Clock CLOCK = Clock.system(ZoneOffset.UTC);
+
   private S3SecurityUtil() {
   }
 
@@ -54,6 +58,19 @@ private S3SecurityUtil() {
   public static void validateS3Credential(OMRequest omRequest,
       OzoneManager ozoneManager) throws ServiceException, OMException {
     if (ozoneManager.isSecurityEnabled()) {
+      // If STS session token is present, decode, decrypt and validate it once 
and save in thread-local
+      if (omRequest.getS3Authentication().hasSessionToken()) {
+        final String token = omRequest.getS3Authentication().getSessionToken();
+        if (!token.isEmpty()) {
+          final STSTokenIdentifier stsTokenIdentifier = 
STSSecurityUtil.constructValidateAndDecryptSTSToken(
+              token, ozoneManager.getSecretKeyClient(), CLOCK);
+          // HMAC signature and expiration were validated above.  Now validate 
AWS signature.
+          validateSTSTokenAwsSignature(stsTokenIdentifier, omRequest);
+          OzoneManager.setStsTokenIdentifier(stsTokenIdentifier);
+          return;
+        }
+      }
+
       OzoneTokenIdentifier s3Token = constructS3Token(omRequest);
       try {
         // authenticate user with signature verification through
@@ -89,4 +106,22 @@ private static OzoneTokenIdentifier 
constructS3Token(OMRequest omRequest) {
     s3Token.setOwner(new Text(auth.getAccessId()));
     return s3Token;
   }
+
+  /**
+   * Validates the AWS signature of an STSTokenIdentifier that has already 
been decrypted.
+   * @param stsTokenIdentifier      the decrypted STS token
+   * @param omRequest               the OMRequest containing STS token
+   * @throws OMException            if the AWS signature validation fails
+   */
+  private static void validateSTSTokenAwsSignature(STSTokenIdentifier 
stsTokenIdentifier, OMRequest omRequest)
+      throws OMException {
+    final String secretAccessKey = stsTokenIdentifier.getSecretAccessKey();
+    final S3Authentication s3Authentication = omRequest.getS3Authentication();
+    if (AWSV4AuthValidator.validateRequest(
+        s3Authentication.getStringToSign(), s3Authentication.getSignature(), 
secretAccessKey)) {
+      return;
+    }
+    throw new OMException(
+        "STS token validation failed for token: " + 
omRequest.getS3Authentication().getSessionToken(), INVALID_TOKEN);
+  }
 }
diff --git 
a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/security/STSSecurityUtil.java
 
b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/security/STSSecurityUtil.java
new file mode 100644
index 00000000000..c3fb14d24b1
--- /dev/null
+++ 
b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/security/STSSecurityUtil.java
@@ -0,0 +1,153 @@
+/*
+ * 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.ozone.security;
+
+import static 
org.apache.hadoop.ozone.om.exceptions.OMException.ResultCodes.INVALID_TOKEN;
+
+import com.google.protobuf.InvalidProtocolBufferException;
+import java.io.IOException;
+import java.time.Clock;
+import java.util.UUID;
+import org.apache.hadoop.hdds.annotation.InterfaceAudience;
+import org.apache.hadoop.hdds.annotation.InterfaceStability;
+import org.apache.hadoop.hdds.security.symmetric.ManagedSecretKey;
+import org.apache.hadoop.hdds.security.symmetric.SecretKeyClient;
+import org.apache.hadoop.ozone.om.exceptions.OMException;
+import 
org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.OMTokenProto;
+import org.apache.hadoop.security.token.SecretManager;
+import org.apache.hadoop.security.token.Token;
+
+/**
+ * Utility class with methods to validate and decrypt STS tokens.
+ */
[email protected]
[email protected]
+public final class STSSecurityUtil {
+  private STSSecurityUtil() {
+  }
+
+  /**
+   * Constructs, validates and decrypts STS session token.
+   *
+   * @param sessionToken    the session token from the x-amz-security-token 
header
+   * @param secretKeyClient the Ozone Manager secretKeyClient
+   * @param clock           the system clock
+   * @return the STSTokenIdentifier with decrypted secretAccessKey
+   * @throws OMException if the token is not valid or processing failed 
otherwise
+   */
+  public static STSTokenIdentifier constructValidateAndDecryptSTSToken(String 
sessionToken,
+      SecretKeyClient secretKeyClient, Clock clock) throws OMException {
+    try {
+      final Token<STSTokenIdentifier> token = 
decodeTokenFromString(sessionToken);
+      return verifyAndDecryptToken(token, secretKeyClient, clock);
+    } catch (SecretManager.InvalidToken e) {
+      throw new OMException("Invalid STS token format: " + e.getMessage(), e, 
INVALID_TOKEN);
+    }
+  }
+
+  /**
+   * Verifies an STS Token by performing multiple checks.
+   *
+   * @param token the token to verify
+   * @param clock the system clock
+   * @return the STSTokenIdentifier with decrypted secretAccessKey
+   * @throws SecretManager.InvalidToken if the token is invalid
+   */
+  private static STSTokenIdentifier 
verifyAndDecryptToken(Token<STSTokenIdentifier> token,
+      SecretKeyClient secretKeyClient, Clock clock) throws 
SecretManager.InvalidToken {
+    if (!STSTokenIdentifier.KIND_NAME.equals(token.getKind())) {
+      throw new SecretManager.InvalidToken("Invalid STS token - kind is 
incorrect: " + token.getKind());
+    }
+
+    if (!STSTokenIdentifier.STS_SERVICE.equals(token.getService().toString())) 
{
+      throw new SecretManager.InvalidToken("Invalid STS token - service is 
incorrect: " + token.getService());
+    }
+
+    final byte[] tokenBytes = token.getIdentifier();
+    final OMTokenProto proto;
+    try {
+      proto = OMTokenProto.parseFrom(tokenBytes);
+    } catch (InvalidProtocolBufferException e) {
+      throw new SecretManager.InvalidToken("Invalid STS token - could not 
parse protocol buffer: " + e.getMessage());
+    }
+    final UUID secretKeyId;
+    try {
+      secretKeyId = UUID.fromString(proto.getSecretKeyId());
+    } catch (IllegalArgumentException e) {
+      throw new SecretManager.InvalidToken("Invalid STS token - secretKeyId 
was not valid: " + proto.getSecretKeyId());
+    }
+
+    final STSTokenIdentifier tokenId = new STSTokenIdentifier();
+    final ManagedSecretKey secretKey;
+    try {
+      secretKey = getValidatedSecretKey(secretKeyId, secretKeyClient);
+      tokenId.setEncryptionKey(secretKey.getSecretKey().getEncoded());
+      tokenId.readFromByteArray(tokenBytes);
+    } catch (IOException e) {
+      throw new SecretManager.InvalidToken("Invalid STS token - could not 
readFromByteArray: " + e.getMessage());
+    }
+
+    // Check expiration
+    if (tokenId.isExpired(clock.instant())) {
+      throw new SecretManager.InvalidToken("Invalid STS token - token expired 
at " + tokenId.getExpiry());
+    }
+
+    // Verify token signature against the original identifier bytes
+    if (!secretKey.isValidSignature(tokenBytes, token.getPassword())) {
+      throw new SecretManager.InvalidToken("Invalid STS token - signature is 
not correct for token: " + tokenId);
+    }
+
+    return tokenId;
+  }
+
+  private static ManagedSecretKey getValidatedSecretKey(UUID secretKeyId, 
SecretKeyClient secretKeyClient)
+      throws SecretManager.InvalidToken {
+    if (secretKeyId == null) {
+      throw new SecretManager.InvalidToken("STS token missing secret key ID");
+    }
+
+    final ManagedSecretKey secretKey;
+    try {
+      secretKey = secretKeyClient.getSecretKey(secretKeyId);
+    } catch (Exception e) {
+      throw new SecretManager.InvalidToken("Failed to retrieve secret key: " + 
e.getMessage());
+    }
+
+    if (secretKey == null) {
+      throw new SecretManager.InvalidToken("Secret key not found for STS token 
secretKeyId: " + secretKeyId);
+    }
+
+    if (secretKey.isExpired()) {
+      throw new SecretManager.InvalidToken("Token cannot be verified due to 
expired secret key " + secretKeyId);
+    }
+
+    return secretKey;
+  }
+
+  private static Token<STSTokenIdentifier> decodeTokenFromString(String 
encodedToken)
+      throws SecretManager.InvalidToken {
+    final Token<STSTokenIdentifier> token = new Token<>();
+    try {
+      token.decodeFromUrlString(encodedToken);
+      return token;
+    } catch (IOException e) {
+      throw new SecretManager.InvalidToken("Failed to decode STS token string: 
" + e);
+    }
+  }
+}
+
diff --git 
a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/security/STSTokenSecretManager.java
 
b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/security/STSTokenSecretManager.java
index 598a5a71675..f72b1892de8 100644
--- 
a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/security/STSTokenSecretManager.java
+++ 
b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/security/STSTokenSecretManager.java
@@ -18,12 +18,14 @@
 package org.apache.hadoop.ozone.security;
 
 import java.io.IOException;
+import java.time.Clock;
 import java.time.Instant;
 import org.apache.hadoop.hdds.annotation.InterfaceAudience;
 import org.apache.hadoop.hdds.annotation.InterfaceStability;
 import org.apache.hadoop.hdds.security.symmetric.ManagedSecretKey;
 import org.apache.hadoop.hdds.security.symmetric.SecretKeySignerClient;
 import org.apache.hadoop.hdds.security.token.ShortLivedTokenSecretManager;
+import org.apache.hadoop.io.Text;
 import org.apache.hadoop.security.token.Token;
 
 /**
@@ -49,6 +51,25 @@ public STSTokenSecretManager(SecretKeySignerClient 
secretKeyClient) {
     this.secretKeyClient = secretKeyClient;
   }
 
+  /**
+   * Override token generation so that we first compute the identifier bytes, 
then sign exactly those bytes, and
+   * return a Token that contains those same identifier bytes. This avoids 
non-determinism from multiple serializations
+   * which would break unit tests.  If we used the inherited generateToken() 
in ShortLivedTokenSecretManager, it
+   * would have made two serialization calls:
+   *   1) in the call to secretKey.sign() in the createPassword() method
+   *   2) in the call to tokenIdentifier.getBytes() for the Token constructor
+   * These two calls would produce different secretAccessKey encrypted values 
because of the random initialization
+   * vector and random salt and hence give non-deterministic return value, so 
here we are only serializing once.
+   */
+  @Override
+  public Token<STSTokenIdentifier> generateToken(STSTokenIdentifier 
tokenIdentifier) {
+    final ManagedSecretKey secretKey = secretKeyClient.getCurrentSecretKey();
+    tokenIdentifier.setSecretKeyId(secretKey.getId());
+    final byte[] identifierBytes = tokenIdentifier.getBytes();
+    final byte[] password = secretKey.sign(identifierBytes);
+    return new Token<>(identifierBytes, password, tokenIdentifier.getKind(), 
new Text(tokenIdentifier.getService()));
+  }
+
   /**
    * Create an STS token and return it as an encoded string.
    *
@@ -59,11 +80,12 @@ public STSTokenSecretManager(SecretKeySignerClient 
secretKeyClient) {
    * @param secretAccessKey     the secret access key associated with the 
temporary access key ID
    * @param sessionPolicy       an optional opaque identifier that further 
limits the scope of
    *                            the permissions granted by the role
+   * @param clock               the system clock
    * @return base64 encoded token string
    */
   public String createSTSTokenString(String tempAccessKeyId, String 
originalAccessKeyId, String roleArn,
-      int durationSeconds, String secretAccessKey, String sessionPolicy) 
throws IOException {
-    final Instant expiration = Instant.now().plusSeconds(durationSeconds);
+      int durationSeconds, String secretAccessKey, String sessionPolicy, Clock 
clock) throws IOException {
+    final Instant expiration = clock.instant().plusSeconds(durationSeconds);
 
     // Get the current secret key for encryption
     final ManagedSecretKey currentSecretKey = 
secretKeyClient.getCurrentSecretKey();
diff --git 
a/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/TestOzoneManagerS3Auth.java
 
b/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/TestOzoneManagerS3Auth.java
new file mode 100644
index 00000000000..8ce4ee08beb
--- /dev/null
+++ 
b/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/TestOzoneManagerS3Auth.java
@@ -0,0 +1,123 @@
+/*
+ * 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.ozone.om;
+
+import static 
org.apache.hadoop.ozone.om.exceptions.OMException.ResultCodes.INVALID_REQUEST;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import org.apache.hadoop.ozone.om.exceptions.OMException;
+import 
org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.S3Authentication;
+import org.apache.hadoop.ozone.security.STSTokenIdentifier;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Test S3 Authentication logic in OzoneManager.
+ */
+public class TestOzoneManagerS3Auth {
+
+  @AfterEach
+  public void tearDown() {
+    OzoneManager.setS3Auth(null);
+    OzoneManager.setStsTokenIdentifier(null);
+  }
+
+  @Test
+  public void testGetS3AuthEffectiveAccessIdNoS3Auth() throws Exception {
+    OzoneManager.setS3Auth(null);
+    assertNull(OzoneManager.getS3AuthEffectiveAccessId());
+  }
+
+  @Test
+  public void testGetS3AuthEffectiveAccessIdNormal() throws Exception {
+    final String accessId = "accessId";
+    final S3Authentication s3Auth = S3Authentication.newBuilder()
+        .setAccessId(accessId)
+        .setSignature("signature")
+        .setStringToSign("stringToSign")
+        .build();
+    OzoneManager.setS3Auth(s3Auth);
+
+    assertEquals(accessId, OzoneManager.getS3AuthEffectiveAccessId());
+  }
+
+  @Test
+  public void testGetS3AuthEffectiveAccessIdWithSessionToken() throws 
Exception {
+    final String tempAccessId = "ASIA12345";
+    final String originalAccessId = "AKIAORIG98765";
+    final String sessionToken = "sessionToken";
+
+    final S3Authentication s3Auth = S3Authentication.newBuilder()
+        .setAccessId(tempAccessId)
+        .setSignature("signature")
+        .setStringToSign("stringToSign")
+        .setSessionToken(sessionToken)
+        .build();
+    OzoneManager.setS3Auth(s3Auth);
+
+    final STSTokenIdentifier stsToken = mock(STSTokenIdentifier.class);
+    when(stsToken.getOriginalAccessKeyId()).thenReturn(originalAccessId);
+    OzoneManager.setStsTokenIdentifier(stsToken);
+
+    assertEquals(originalAccessId, OzoneManager.getS3AuthEffectiveAccessId());
+  }
+
+  @Test
+  public void testGetS3AuthEffectiveAccessIdWithEmptySessionToken() throws 
Exception {
+    final String accessId = "AKIAORIG98765";
+    final String emptySessionToken = "";
+
+    final S3Authentication s3Auth = S3Authentication.newBuilder()
+        .setAccessId(accessId)
+        .setSignature("signature")
+        .setStringToSign("stringToSign")
+        .setSessionToken(emptySessionToken)
+        .build();
+    OzoneManager.setS3Auth(s3Auth);
+
+    // Empty session token should be treated as if no session token is present.
+    assertEquals(accessId, OzoneManager.getS3AuthEffectiveAccessId());
+  }
+  
+  @Test
+  public void 
testGetS3AuthEffectiveAccessIdWithSessionTokenMissingOriginalAccessKey() {
+    final String tempAccessId = "ASIA12345";
+    final String sessionToken = "sessionToken";
+
+    final S3Authentication s3Auth = S3Authentication.newBuilder()
+        .setAccessId(tempAccessId)
+        .setSignature("signature")
+        .setStringToSign("stringToSign")
+        .setSessionToken(sessionToken)
+        .build();
+    OzoneManager.setS3Auth(s3Auth);
+
+    final STSTokenIdentifier stsToken = mock(STSTokenIdentifier.class);
+    when(stsToken.getOriginalAccessKeyId()).thenReturn(null); // Missing 
original ID
+    OzoneManager.setStsTokenIdentifier(stsToken);
+
+    final OMException ex = assertThrows(OMException.class, 
OzoneManager::getS3AuthEffectiveAccessId);
+    assertEquals(INVALID_REQUEST, ex.getResult());
+    assertEquals("Invalid STS Token format - could not find 
originalAccessKeyId", ex.getMessage());
+  }
+}
+
diff --git 
a/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/request/TestOMClientRequestWithUserInfo.java
 
b/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/request/TestOMClientRequestWithUserInfo.java
index 9fda60374c1..d891a3bc8b9 100644
--- 
a/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/request/TestOMClientRequestWithUserInfo.java
+++ 
b/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/request/TestOMClientRequestWithUserInfo.java
@@ -22,6 +22,7 @@
 import static 
org.apache.hadoop.ozone.om.request.OMRequestTestUtils.newCreateBucketRequest;
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertThrows;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.mockStatic;
@@ -47,6 +48,8 @@
 import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos;
 import 
org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.BucketInfo;
 import 
org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.OMRequest;
+import 
org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.S3Authentication;
+import org.apache.hadoop.ozone.security.STSTokenIdentifier;
 import org.apache.hadoop.security.UserGroupInformation;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
@@ -163,4 +166,138 @@ public void testUserInfoInCaseOfGrpcTransport() throws 
IOException {
     }
   }
 
+  @Test
+  public void testUserInfoWithSTSToken() throws IOException {
+    final String accessId = "ASIA12345";
+    final String signature = "Signature";
+    final String stringToSign = "StringToSign";
+    final String sessionToken = "SessionToken";
+    final String originalAccessKeyId = "AKIAORIGINAL";
+
+    final STSTokenIdentifier stsTokenIdentifier = 
mock(STSTokenIdentifier.class);
+    
when(stsTokenIdentifier.getOriginalAccessKeyId()).thenReturn(originalAccessKeyId);
+
+    final S3Authentication s3Authentication = S3Authentication.newBuilder()
+        .setAccessId(accessId)
+        .setSignature(signature)
+        .setStringToSign(stringToSign)
+        .setSessionToken(sessionToken)
+        .build();
+
+    OzoneManager.setS3Auth(s3Authentication);
+    OzoneManager.setStsTokenIdentifier(stsTokenIdentifier);
+
+    try {
+      final OMRequest.Builder omRequestBuilder = OMRequest.newBuilder()
+          .setCmdType(OzoneManagerProtocolProtos.Type.CommitKey)
+          .setClientId(UUID.randomUUID().toString())
+          .setS3Authentication(s3Authentication);
+
+      final OMRequest omRequest = omRequestBuilder.build();
+      final OMClientRequest omClientRequest = new 
OMKeyCommitRequest(omRequest, mock(BucketLayout.class));
+
+      final OzoneManagerProtocolProtos.UserInfo userInfo = 
omClientRequest.getUserInfo();
+      assertEquals(originalAccessKeyId, userInfo.getUserName());
+    } finally {
+      OzoneManager.setStsTokenIdentifier(null);
+      OzoneManager.setS3Auth(null);
+    }
+  }
+
+  @Test
+  public void testUserInfoWithSTSAccessKeyMissingSessionToken() {
+    final String accessId = "ASIA12345";
+    final String signature = "Signature";
+    final String stringToSign = "StringToSign";
+
+    final OMRequest omRequest = OMRequest.newBuilder()
+        .setCmdType(OzoneManagerProtocolProtos.Type.CommitKey)
+        .setClientId(UUID.randomUUID().toString())
+        .setS3Authentication(S3Authentication.newBuilder()
+            .setAccessId(accessId)
+            .setSignature(signature)
+            .setStringToSign(stringToSign)
+            .build())
+        .build();
+
+    final OMClientRequest omClientRequest = new OMKeyCommitRequest(omRequest, 
mock(BucketLayout.class));
+    final IOException ex = assertThrows(IOException.class, 
omClientRequest::getUserInfo);
+
+    assertEquals("Error with STS token", ex.getMessage());
+    assertEquals("Missing session token for accessKeyId: " + accessId, 
ex.getCause().getMessage());
+  }
+
+  @Test
+  public void testUserInfoWithSessionTokenButNoStsTokenIdentifier() {
+    final String accessId = "ASIA12345";
+    final String signature = "Signature";
+    final String stringToSign = "StringToSign";
+    final String sessionToken = "SessionToken";
+
+    final S3Authentication s3Authentication = S3Authentication.newBuilder()
+        .setAccessId(accessId)
+        .setSignature(signature)
+        .setStringToSign(stringToSign)
+        .setSessionToken(sessionToken)
+        .build();
+
+    final OMRequest omRequest = OMRequest.newBuilder()
+        .setCmdType(OzoneManagerProtocolProtos.Type.CommitKey)
+        .setClientId(UUID.randomUUID().toString())
+        .setS3Authentication(s3Authentication)
+        .build();
+
+    OzoneManager.setS3Auth(s3Authentication);
+    OzoneManager.setStsTokenIdentifier(null);
+
+    try {
+      final OMClientRequest omClientRequest = new 
OMKeyCommitRequest(omRequest, mock(BucketLayout.class));
+      final IOException ex = assertThrows(IOException.class, 
omClientRequest::getUserInfo);
+
+      assertEquals("Error with STS Token", ex.getMessage());
+      assertEquals(
+          "OMClientRequest has session token but no token identifier in 
OzoneManager", ex.getCause().getMessage());
+    } finally {
+      OzoneManager.setS3Auth(null);
+    }
+  }
+
+  @Test
+  public void testUserInfoWithSessionTokenButEmptyOriginalAccessKeyId() {
+    final String accessId = "ASIA12345";
+    final String signature = "Signature";
+    final String stringToSign = "StringToSign";
+    final String sessionToken = "SessionToken";
+
+    final STSTokenIdentifier stsTokenIdentifier = 
mock(STSTokenIdentifier.class);
+    when(stsTokenIdentifier.getOriginalAccessKeyId()).thenReturn("");
+
+    final S3Authentication s3Authentication = S3Authentication.newBuilder()
+        .setAccessId(accessId)
+        .setSignature(signature)
+        .setStringToSign(stringToSign)
+        .setSessionToken(sessionToken)
+        .build();
+
+    OzoneManager.setS3Auth(s3Authentication);
+    OzoneManager.setStsTokenIdentifier(stsTokenIdentifier);
+
+    try {
+      final OMRequest omRequest = OMRequest.newBuilder()
+          .setCmdType(OzoneManagerProtocolProtos.Type.CommitKey)
+          .setClientId(UUID.randomUUID().toString())
+          .setS3Authentication(s3Authentication)
+          .build();
+
+      final OMClientRequest omClientRequest = new 
OMKeyCommitRequest(omRequest, mock(BucketLayout.class));
+      final IOException ex = assertThrows(IOException.class, 
omClientRequest::getUserInfo);
+
+      assertEquals("Error with STS Token", ex.getMessage());
+      assertEquals("Invalid STS Token format - could not find 
originalAccessKeyId", ex.getCause().getMessage());
+    } finally {
+      OzoneManager.setS3Auth(null);
+      OzoneManager.setStsTokenIdentifier(null);
+    }
+  }
+
 }
diff --git 
a/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/security/TestSTSSecurityUtil.java
 
b/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/security/TestSTSSecurityUtil.java
new file mode 100644
index 00000000000..96c83287705
--- /dev/null
+++ 
b/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/security/TestSTSSecurityUtil.java
@@ -0,0 +1,318 @@
+/*
+ * 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.ozone.security;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.time.Instant;
+import java.time.ZoneOffset;
+import java.util.UUID;
+import org.apache.hadoop.hdds.security.exception.SCMSecurityException;
+import org.apache.hadoop.hdds.security.symmetric.ManagedSecretKey;
+import org.apache.hadoop.hdds.security.symmetric.SecretKeyClient;
+import org.apache.hadoop.io.Text;
+import org.apache.hadoop.ozone.om.exceptions.OMException;
+import 
org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.OMTokenProto;
+import org.apache.hadoop.security.token.Token;
+import org.apache.ozone.test.TestClock;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Unit tests for STSSecurityUtil.
+ */
+public class TestSTSSecurityUtil {
+  private static final String TEMP_ACCESS_KEY = "temp-access-key";
+  private static final String ORIGINAL_ACCESS_KEY = "original-access-key";
+  private static final String ROLE_ARN = 
"arn:aws:iam::123456789012:role/test-role";
+  private static final String SECRET_ACCESS_KEY = "test-secret-access-key";
+  private static final String SESSION_POLICY = "test-session-policy";
+  private static final int DURATION_SECONDS = 3600;
+
+  private final SecretKeyTestClient secretKeyClient = new 
SecretKeyTestClient();
+  private final STSTokenSecretManager tokenSecretManager = new 
STSTokenSecretManager(secretKeyClient);
+  private final UUID secretKeyId = 
secretKeyClient.getCurrentSecretKey().getId();
+  private final TestClock clock = new 
TestClock(Instant.ofEpochMilli(1764819000), ZoneOffset.UTC);
+
+  @Test
+  public void testConstructValidateAndDecryptSTSTokenInvalidProtobuf() throws 
IOException {
+    // Create a token whose identifier bytes are not a valid OMTokenProto
+    final Token<STSTokenIdentifier> token = new Token<>(
+        new byte[] {0x01, 0x02, 0x03}, new byte[] {0x04}, 
STSTokenIdentifier.KIND_NAME,
+        new Text(STSTokenIdentifier.STS_SERVICE));
+
+    final String tokenString = token.encodeToUrlString();
+
+    assertThatThrownBy(() ->
+        STSSecurityUtil.constructValidateAndDecryptSTSToken(tokenString, 
secretKeyClient, clock))
+        .isInstanceOf(OMException.class)
+        .hasMessage(
+            "Invalid STS token format: Invalid STS token - could not parse 
protocol buffer: Protocol message " +
+            "contained an invalid tag (zero).");
+  }
+
+  @Test
+  public void testConstructValidateAndDecryptSTSTokenSuccess() throws 
IOException {
+    // Create a valid token
+    final String tokenString = tokenSecretManager.createSTSTokenString(
+        TEMP_ACCESS_KEY, ORIGINAL_ACCESS_KEY, ROLE_ARN, DURATION_SECONDS, 
SECRET_ACCESS_KEY, SESSION_POLICY, clock);
+
+    // Validate and decrypt the token
+    final STSTokenIdentifier result = 
STSSecurityUtil.constructValidateAndDecryptSTSToken(
+        tokenString, secretKeyClient, clock);
+
+    // Verify the result
+    assertThat(result.getOwnerId()).isEqualTo(TEMP_ACCESS_KEY);
+    assertThat(result.getOriginalAccessKeyId()).isEqualTo(ORIGINAL_ACCESS_KEY);
+    assertThat(result.getRoleArn()).isEqualTo(ROLE_ARN);
+    assertThat(result.getSecretAccessKey()).isEqualTo(SECRET_ACCESS_KEY);
+    assertThat(result.getSessionPolicy()).isEqualTo(SESSION_POLICY);
+    assertThat(result.isExpired(clock.instant())).isFalse();
+    final long expirationEpochMillis = result.getExpiry().toEpochMilli();
+    assertThat(expirationEpochMillis).isEqualTo(clock.millis() + 
(DURATION_SECONDS * 1000));
+  }
+
+  @Test
+  public void 
testConstructValidateAndDecryptSTSTokenSuccessWithNullSessionPolicy() throws 
Exception {
+    // Create a valid token with null session policy
+    final String tokenString = tokenSecretManager.createSTSTokenString(
+        TEMP_ACCESS_KEY, ORIGINAL_ACCESS_KEY, ROLE_ARN, DURATION_SECONDS, 
SECRET_ACCESS_KEY, null, clock);
+
+    // Validate and decrypt the token
+    final STSTokenIdentifier result = 
STSSecurityUtil.constructValidateAndDecryptSTSToken(
+        tokenString, secretKeyClient, clock);
+
+    // Verify the result
+    assertThat(result.getSessionPolicy()).isEmpty();
+  }
+
+  @Test
+  public void testConstructValidateAndDecryptSTSTokenInvalidFormat() {
+    // Try to decrypt an invalid token string
+    assertThatThrownBy(() ->
+        
STSSecurityUtil.constructValidateAndDecryptSTSToken("invalid-token-format", 
secretKeyClient, clock))
+        .isInstanceOf(OMException.class)
+        .hasMessageContaining("Invalid STS token format: Failed to decode STS 
token string");
+  }
+
+  @Test
+  public void testConstructValidateAndDecryptSTSTokenInvalidKind() throws 
Exception {
+    // Create a valid identifier to use as base
+    final String validTokenString = tokenSecretManager.createSTSTokenString(
+        TEMP_ACCESS_KEY, ORIGINAL_ACCESS_KEY, ROLE_ARN, DURATION_SECONDS, 
SECRET_ACCESS_KEY, SESSION_POLICY, clock);
+
+    final Token<STSTokenIdentifier> validToken = new Token<>();
+    validToken.decodeFromUrlString(validTokenString);
+
+    // Create token with wrong kind
+    final Token<STSTokenIdentifier> token = new Token<>(
+        validToken.getIdentifier(), validToken.getPassword(), new 
Text("WRONG_KIND"),
+        new Text(STSTokenIdentifier.STS_SERVICE));
+
+    final String invalidTokenString = token.encodeToUrlString();
+
+    // Try to validate the token with wrong kind
+    assertThatThrownBy(() ->
+        
STSSecurityUtil.constructValidateAndDecryptSTSToken(invalidTokenString, 
secretKeyClient, clock))
+        .isInstanceOf(OMException.class)
+        .hasMessage("Invalid STS token format: Invalid STS token - kind is 
incorrect: WRONG_KIND");
+  }
+
+  @Test
+  public void testConstructValidateAndDecryptSTSTokenInvalidService() throws 
Exception {
+    // Create a token with incorrect service
+    final String validTokenString = tokenSecretManager.createSTSTokenString(
+        TEMP_ACCESS_KEY, ORIGINAL_ACCESS_KEY, ROLE_ARN, DURATION_SECONDS, 
SECRET_ACCESS_KEY, SESSION_POLICY, clock);
+
+    final Token<STSTokenIdentifier> validToken = new Token<>();
+    validToken.decodeFromUrlString(validTokenString);
+
+    final Token<STSTokenIdentifier> token = new Token<>(
+        validToken.getIdentifier(), validToken.getPassword(), 
validToken.getKind(), new Text("WRONG_SERVICE"));
+
+    final String invalidTokenString = token.encodeToUrlString();
+
+    // Try to validate the token with wrong service
+    assertThatThrownBy(() ->
+        
STSSecurityUtil.constructValidateAndDecryptSTSToken(invalidTokenString, 
secretKeyClient, clock))
+        .isInstanceOf(OMException.class)
+        .hasMessage("Invalid STS token format: Invalid STS token - service is 
incorrect: WRONG_SERVICE");
+  }
+
+  @Test
+  public void testConstructValidateAndDecryptSTSTokenExpired() throws 
Exception {
+    // Create a token that expires immediately (durationSeconds of 0)
+    final String tokenString = tokenSecretManager.createSTSTokenString(
+        TEMP_ACCESS_KEY, ORIGINAL_ACCESS_KEY, ROLE_ARN, 0, SECRET_ACCESS_KEY, 
SESSION_POLICY, clock);
+
+    // Fast-forward time to ensure token is expired
+    clock.fastForward(100);
+
+    // Try to validate the expired token
+    assertThatThrownBy(() ->
+        STSSecurityUtil.constructValidateAndDecryptSTSToken(tokenString, 
secretKeyClient, clock))
+        .isInstanceOf(OMException.class)
+        .hasMessageContaining("Invalid STS token format: Invalid STS token - 
token expired at");
+  }
+
+  @Test
+  public void testConstructValidateAndDecryptSTSTokenSecretKeyNotFound() 
throws Exception {
+    // Create a valid token string
+    final String validTokenString = tokenSecretManager.createSTSTokenString(
+        TEMP_ACCESS_KEY, ORIGINAL_ACCESS_KEY, ROLE_ARN, DURATION_SECONDS, 
SECRET_ACCESS_KEY, SESSION_POLICY, clock);
+
+    // Create a mock secret key client that returns null for the key
+    final SecretKeyClient mockKeyClient = mock(SecretKeyClient.class);
+    when(mockKeyClient.getSecretKey(any())).thenReturn(null);
+
+    // Try to validate the token when secret key is not found
+    assertThatThrownBy(() ->
+        STSSecurityUtil.constructValidateAndDecryptSTSToken(validTokenString, 
mockKeyClient, clock))
+        .isInstanceOf(OMException.class)
+        .hasMessage(
+            "Invalid STS token format: Invalid STS token - could not 
readFromByteArray: Secret key not found for " +
+            "STS token secretKeyId: " + secretKeyId);
+  }
+
+  @Test
+  public void testConstructValidateAndDecryptSTSTokenInvalidSecretKeyId() 
throws Exception {
+    // Create a valid identifier to use as base
+    final String validTokenString = tokenSecretManager.createSTSTokenString(
+        TEMP_ACCESS_KEY, ORIGINAL_ACCESS_KEY, ROLE_ARN, DURATION_SECONDS, 
SECRET_ACCESS_KEY, SESSION_POLICY, clock);
+
+    final Token<STSTokenIdentifier> validToken = new Token<>();
+    validToken.decodeFromUrlString(validTokenString);
+
+    // Rewrite the identifier with an invalid secretKeyId in the protobuf
+    final byte[] identifierBytes = validToken.getIdentifier();
+    final OMTokenProto proto = OMTokenProto.parseFrom(identifierBytes);
+
+    final OMTokenProto invalidProto = 
proto.toBuilder().setSecretKeyId("not-a-uuid").build();
+
+    final Token<STSTokenIdentifier> brokenToken = new Token<>(
+        invalidProto.toByteArray(), validToken.getPassword(), 
validToken.getKind(), validToken.getService());
+
+    final String invalidTokenString = brokenToken.encodeToUrlString();
+
+    assertThatThrownBy(() ->
+        
STSSecurityUtil.constructValidateAndDecryptSTSToken(invalidTokenString, 
secretKeyClient, clock))
+        .isInstanceOf(OMException.class)
+        .hasMessage("Invalid STS token format: Invalid STS token - secretKeyId 
was not valid: not-a-uuid");
+  }
+
+  @Test
+  public void testConstructValidateAndDecryptSTSTokenExpiredSecretKey() throws 
Exception {
+    // Create a valid token string
+    final String validTokenString = tokenSecretManager.createSTSTokenString(
+        TEMP_ACCESS_KEY, ORIGINAL_ACCESS_KEY, ROLE_ARN, DURATION_SECONDS, 
SECRET_ACCESS_KEY, SESSION_POLICY, clock);
+
+    // Create a mock secret key that is expired
+    final ManagedSecretKey expiredSecretKey = mock(ManagedSecretKey.class);
+    when(expiredSecretKey.isExpired()).thenReturn(true);
+
+    final SecretKeyClient mockKeyClient = mock(SecretKeyClient.class);
+    when(mockKeyClient.getSecretKey(any())).thenReturn(expiredSecretKey);
+
+    // Try to validate the token with expired secret key
+    assertThatThrownBy(() ->
+        STSSecurityUtil.constructValidateAndDecryptSTSToken(validTokenString, 
mockKeyClient, clock))
+        .isInstanceOf(OMException.class)
+        .hasMessage(
+            "Invalid STS token format: Invalid STS token - could not 
readFromByteArray: Token cannot be " +
+            "verified due to expired secret key " + secretKeyId);
+  }
+
+  @Test
+  public void 
testConstructValidateAndDecryptSTSTokenSecretKeyRetrievalException() throws 
Exception {
+    // Create a valid token string
+    final String validTokenString = tokenSecretManager.createSTSTokenString(
+        TEMP_ACCESS_KEY, ORIGINAL_ACCESS_KEY, ROLE_ARN, DURATION_SECONDS, 
SECRET_ACCESS_KEY, SESSION_POLICY, clock);
+
+    // Create a mock secret key client that throws an exception
+    final SecretKeyClient mockKeyClient = mock(SecretKeyClient.class);
+    when(mockKeyClient.getSecretKey(any())).thenThrow(new 
SCMSecurityException("something went wrong"));
+
+    // Try to validate the token when secret key retrieval fails
+    assertThatThrownBy(() ->
+        STSSecurityUtil.constructValidateAndDecryptSTSToken(validTokenString, 
mockKeyClient, clock))
+        .isInstanceOf(OMException.class)
+        .hasMessage(
+            "Invalid STS token format: Invalid STS token - could not 
readFromByteArray: Failed to retrieve secret " +
+            "key: something went wrong");
+  }
+
+  @Test
+  public void testConstructValidateAndDecryptSTSTokenInvalidSignature() throws 
Exception {
+    // Create a valid token string
+    final String validTokenString = tokenSecretManager.createSTSTokenString(
+        TEMP_ACCESS_KEY, ORIGINAL_ACCESS_KEY, ROLE_ARN, DURATION_SECONDS, 
SECRET_ACCESS_KEY, SESSION_POLICY, clock);
+
+    final Token<STSTokenIdentifier> validToken = new Token<>();
+    validToken.decodeFromUrlString(validTokenString);
+
+    // Create a token with invalid signature (wrong password)
+    final Token<STSTokenIdentifier> invalidToken = new Token<>(
+        validToken.getIdentifier(), 
"wrong-signature".getBytes(StandardCharsets.UTF_8), validToken.getKind(),
+        validToken.getService());
+
+    final String invalidTokenString = invalidToken.encodeToUrlString();
+
+    // Try to validate the token with invalid signature
+    assertThatThrownBy(() ->
+        
STSSecurityUtil.constructValidateAndDecryptSTSToken(invalidTokenString, 
secretKeyClient, clock))
+        .isInstanceOf(OMException.class)
+        .hasMessageContaining("Invalid STS token format: Invalid STS token - 
signature is not correct for token");
+  }
+
+  @Test
+  public void testConstructValidateAndDecryptSTSTokenEmptyString() {
+    // Try to decrypt an empty token string
+    assertThatThrownBy(() ->
+        STSSecurityUtil.constructValidateAndDecryptSTSToken("", 
secretKeyClient, clock))
+        .isInstanceOf(OMException.class)
+        .hasMessage("Invalid STS token format: Failed to decode STS token 
string: java.io.EOFException");
+  }
+
+  @Test
+  public void testConstructValidateAndDecryptMultipleTokens() throws Exception 
{
+    // Create multiple tokens and validate them all
+    final String token1 = tokenSecretManager.createSTSTokenString(
+        "temp-key-1", "orig-key-1", "role-arn-1", DURATION_SECONDS,
+        "secret-key-1", "policy-1", clock);
+
+    final String token2 = tokenSecretManager.createSTSTokenString(
+        "temp-key-2", "orig-key-2", "role-arn-2", DURATION_SECONDS,
+        "secret-key-2", "policy-2", clock);
+
+    final STSTokenIdentifier result1 = 
STSSecurityUtil.constructValidateAndDecryptSTSToken(
+        token1, secretKeyClient, clock);
+    final STSTokenIdentifier result2 = 
STSSecurityUtil.constructValidateAndDecryptSTSToken(
+        token2, secretKeyClient, clock);
+
+    assertThat(result1.getOwnerId()).isEqualTo("temp-key-1");
+    assertThat(result1.getOriginalAccessKeyId()).isEqualTo("orig-key-1");
+    assertThat(result2.getOwnerId()).isEqualTo("temp-key-2");
+    assertThat(result2.getOriginalAccessKeyId()).isEqualTo("orig-key-2");
+  }
+}
+
diff --git 
a/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/security/TestSTSTokenSecretManager.java
 
b/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/security/TestSTSTokenSecretManager.java
index 5eb7868c4a2..ad7d3df71ff 100644
--- 
a/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/security/TestSTSTokenSecretManager.java
+++ 
b/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/security/TestSTSTokenSecretManager.java
@@ -27,6 +27,7 @@
 import java.io.IOException;
 import java.nio.charset.StandardCharsets;
 import java.time.Instant;
+import java.time.ZoneOffset;
 import java.util.UUID;
 import javax.crypto.SecretKey;
 import javax.crypto.spec.SecretKeySpec;
@@ -34,6 +35,7 @@
 import org.apache.hadoop.hdds.security.symmetric.SecretKeySignerClient;
 import org.apache.hadoop.io.Text;
 import org.apache.hadoop.security.token.Token;
+import org.apache.ozone.test.TestClock;
 import org.junit.jupiter.api.BeforeAll;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
@@ -43,6 +45,8 @@
  */
 public class TestSTSTokenSecretManager {
   private STSTokenSecretManager secretManager;
+  private TestClock clock;
+
   private static final String TEMP_ACCESS_KEY = "temp-access-key";
   private static final String ORIGINAL_ACCESS_KEY = "original-access-key";
   private static final String ROLE_ARN = 
"arn:aws:iam::123456789012:role/test-role";
@@ -71,14 +75,13 @@ public void setUp() throws Exception {
     when(mockSecretKeyClient.getCurrentSecretKey()).thenReturn(mockSecretKey);
 
     secretManager = new STSTokenSecretManager(mockSecretKeyClient);
+    clock = new TestClock(Instant.ofEpochMilli(1764819000), ZoneOffset.UTC);
   }
 
   @Test
   public void testCreateSTSTokenStringContainsCorrectFields() throws 
IOException {
-    final Instant beforeCreation = Instant.now();
-
     final String tokenString = secretManager.createSTSTokenString(
-        TEMP_ACCESS_KEY, ORIGINAL_ACCESS_KEY, ROLE_ARN, DURATION_SECONDS, 
SECRET_ACCESS_KEY, SESSION_POLICY);
+        TEMP_ACCESS_KEY, ORIGINAL_ACCESS_KEY, ROLE_ARN, DURATION_SECONDS, 
SECRET_ACCESS_KEY, SESSION_POLICY, clock);
 
     // Decode the token
     final Token<STSTokenIdentifier> token = new Token<>();
@@ -88,7 +91,6 @@ public void testCreateSTSTokenStringContainsCorrectFields() 
throws IOException {
     final STSTokenIdentifier identifier = new STSTokenIdentifier();
     identifier.setEncryptionKey(sharedSecretKey.getEncoded());
     identifier.readFromByteArray(token.getIdentifier());
-    final Instant afterCreation = Instant.now();
     final Instant expiration = identifier.getExpiry();
 
     assertEquals(TEMP_ACCESS_KEY, identifier.getTempAccessKeyId());
@@ -99,15 +101,13 @@ public void 
testCreateSTSTokenStringContainsCorrectFields() throws IOException {
     assertNotNull(identifier.getSecretKeyId());
     assertEquals(new Text("STSToken"), identifier.getKind());
     assertEquals("STS", identifier.getService());
-    // Verify expiration is approximately durationSeconds in the future
-    assertTrue(expiration.isAfter(beforeCreation.plusSeconds(DURATION_SECONDS 
- 1)));
-    assertTrue(expiration.isBefore(afterCreation.plusSeconds(DURATION_SECONDS 
+ 1)));
+    assertEquals(clock.millis() + (DURATION_SECONDS * 1000), 
expiration.toEpochMilli());
   }
 
   @Test
   public void testCreateSTSTokenStringWithNullSessionPolicy() throws 
IOException {
     final String tokenString = secretManager.createSTSTokenString(
-        TEMP_ACCESS_KEY, ORIGINAL_ACCESS_KEY, ROLE_ARN, DURATION_SECONDS, 
SECRET_ACCESS_KEY, null);
+        TEMP_ACCESS_KEY, ORIGINAL_ACCESS_KEY, ROLE_ARN, DURATION_SECONDS, 
SECRET_ACCESS_KEY, null, clock);
 
     // Decode the token
     final Token<STSTokenIdentifier> token = new Token<>();


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to