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

miroslav pushed a commit to branch trunk
in repository https://gitbox.apache.org/repos/asf/jackrabbit-oak.git


The following commit(s) were added to refs/heads/trunk by this push:
     new c2e2cd7add OAK-10780: add azure access token refresh logic (#1441)
c2e2cd7add is described below

commit c2e2cd7addb8ef154d3ac83fe31a56111a0f95c1
Author: Tushar <145645280+t-r...@users.noreply.github.com>
AuthorDate: Fri May 24 12:46:36 2024 +0530

    OAK-10780: add azure access token refresh logic (#1441)
    
    * OAK-10780: add azure access token refresh logic
    
    * OAK-10780: modify log statement
    
    * OAK-10780: modify log statement
    
    * OAK-10780: reduce token refresh interval
---
 .../oak/segment/azure/AzureUtilities.java          | 84 +++++++++++++++++++++-
 .../jackrabbit/oak/segment/azure/package-info.java |  2 +-
 2 files changed, 82 insertions(+), 4 deletions(-)

diff --git 
a/oak-segment-azure/src/main/java/org/apache/jackrabbit/oak/segment/azure/AzureUtilities.java
 
b/oak-segment-azure/src/main/java/org/apache/jackrabbit/oak/segment/azure/AzureUtilities.java
index 5817d78a44..ec5763b0cf 100644
--- 
a/oak-segment-azure/src/main/java/org/apache/jackrabbit/oak/segment/azure/AzureUtilities.java
+++ 
b/oak-segment-azure/src/main/java/org/apache/jackrabbit/oak/segment/azure/AzureUtilities.java
@@ -16,6 +16,7 @@
  */
 package org.apache.jackrabbit.oak.segment.azure;
 
+import com.azure.core.credential.AccessToken;
 import com.azure.core.credential.TokenRequestContext;
 import com.azure.identity.ClientSecretCredential;
 import com.azure.identity.ClientSecretCredentialBuilder;
@@ -32,7 +33,9 @@ import com.microsoft.azure.storage.blob.CloudBlobContainer;
 import com.microsoft.azure.storage.blob.CloudBlobDirectory;
 import com.microsoft.azure.storage.blob.LeaseStatus;
 import com.microsoft.azure.storage.blob.ListBlobItem;
+import org.apache.commons.lang3.StringUtils;
 import org.apache.jackrabbit.oak.commons.Buffer;
+import org.apache.jackrabbit.oak.commons.concurrent.ExecutorCloser;
 import org.apache.jackrabbit.oak.segment.spi.RepositoryNotReachableException;
 import org.jetbrains.annotations.NotNull;
 import org.slf4j.Logger;
@@ -45,20 +48,33 @@ import java.net.URI;
 import java.net.URISyntaxException;
 import java.nio.file.Paths;
 import java.security.InvalidKeyException;
+import java.time.LocalDateTime;
+import java.time.OffsetDateTime;
+import java.time.format.DateTimeFormatter;
 import java.util.ArrayList;
 import java.util.EnumSet;
 import java.util.List;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
 
 public final class AzureUtilities {
+
+    static {
+        Runtime.getRuntime().addShutdownHook(new 
Thread(AzureUtilities::shutDown));
+    }
+
     public static final String AZURE_ACCOUNT_NAME = "AZURE_ACCOUNT_NAME";
     public static final String AZURE_SECRET_KEY = "AZURE_SECRET_KEY";
     public static final String AZURE_TENANT_ID = "AZURE_TENANT_ID";
     public static final String AZURE_CLIENT_ID = "AZURE_CLIENT_ID";
     public static final String AZURE_CLIENT_SECRET = "AZURE_CLIENT_SECRET";
-
     private static final String AZURE_DEFAULT_SCOPE = 
"https://storage.azure.com/.default";;
+    private static final long TOKEN_REFRESHER_INITIAL_DELAY = 45L;
+    private static final long TOKEN_REFRESHER_DELAY = 1L;
 
     private static final Logger log = 
LoggerFactory.getLogger(AzureUtilities.class);
+    private static final ScheduledExecutorService executorService = 
Executors.newSingleThreadScheduledExecutor();
 
     private AzureUtilities() {
     }
@@ -136,8 +152,15 @@ public final class AzureUtilities {
                 .tenantId(tenantId)
                 .build();
 
-        String accessToken = clientSecretCredential.getTokenSync(new 
TokenRequestContext().addScopes(AZURE_DEFAULT_SCOPE)).getToken();
-        return new StorageCredentialsToken(accountName, accessToken);
+        AccessToken accessToken = clientSecretCredential.getTokenSync(new 
TokenRequestContext().addScopes(AZURE_DEFAULT_SCOPE));
+        if (accessToken == null || 
StringUtils.isBlank(accessToken.getToken())) {
+            log.error("Access token is null or empty");
+            throw new IllegalArgumentException("Could not connect to azure 
storage, access token is null or empty");
+        }
+        StorageCredentialsToken storageCredentialsToken = new 
StorageCredentialsToken(accountName, accessToken.getToken());
+        TokenRefresher tokenRefresher = new 
TokenRefresher(clientSecretCredential, accessToken, storageCredentialsToken);
+        executorService.scheduleWithFixedDelay(tokenRefresher, 
TOKEN_REFRESHER_INITIAL_DELAY, TOKEN_REFRESHER_DELAY, TimeUnit.MINUTES);
+        return storageCredentialsToken;
     }
 
     private static ResultSegment<ListBlobItem> 
listBlobsInSegments(CloudBlobDirectory directory,
@@ -207,6 +230,61 @@ public final class AzureUtilities {
         }
     }
 
+    /**
+     * This class represents a token refresher responsible for ensuring the 
validity of the access token used for azure AD authentication.
+     * The access token generated by the Azure client is valid for 1 hour 
only. Therefore, this class periodically checks the validity
+     * of the access token and refreshes it if necessary. The refresh is 
triggered when the current access token is about to expire,
+     * defined by a threshold of 5 minutes from the current time. This 
threshold is similar to what is being used in azure identity to
+     * generate a new token
+     */
+    private static class TokenRefresher implements Runnable {
+
+        private final ClientSecretCredential clientSecretCredential;
+        private AccessToken accessToken;
+        private final StorageCredentialsToken storageCredentialsToken;
+
+
+        /**
+         * Constructs a new TokenRefresher object with the specified 
parameters.
+         *
+         * @param clientSecretCredential  The client secret credential used to 
obtain the access token.
+         * @param accessToken             The current access token.
+         * @param storageCredentialsToken The storage credentials token 
associated with the access token.
+         */
+        public TokenRefresher(ClientSecretCredential clientSecretCredential,
+                              AccessToken accessToken,
+                              StorageCredentialsToken storageCredentialsToken) 
{
+            this.clientSecretCredential = clientSecretCredential;
+            this.accessToken = accessToken;
+            this.storageCredentialsToken = storageCredentialsToken;
+        }
+
+        @Override
+        public void run() {
+            try {
+                log.debug("Checking for azure access token expiry at: {}", 
LocalDateTime.now());
+                OffsetDateTime tokenExpiryThreshold = 
OffsetDateTime.now().plusMinutes(5);
+                if (accessToken.getExpiresAt() != null && 
accessToken.getExpiresAt().isBefore(tokenExpiryThreshold)) {
+                    log.info("Access token is about to expire (5 minutes or 
less) at: {}. New access token will be generated",
+                            
accessToken.getExpiresAt().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME));
+                    AccessToken newToken = 
clientSecretCredential.getTokenSync(new 
TokenRequestContext().addScopes(AZURE_DEFAULT_SCOPE));
+                    if (newToken == null || 
StringUtils.isBlank(newToken.getToken())) {
+                        log.error("New access token is null or empty");
+                        return;
+                    }
+                    this.accessToken = newToken;
+                    
this.storageCredentialsToken.updateToken(this.accessToken.getToken());
+                }
+            } catch (Exception e) {
+                log.error("Error while acquiring new access token: ", e);
+            }
+        }
+    }
+
+    public static void shutDown() {
+        new ExecutorCloser(executorService).close();
+    }
+
 }
 
 
diff --git 
a/oak-segment-azure/src/main/java/org/apache/jackrabbit/oak/segment/azure/package-info.java
 
b/oak-segment-azure/src/main/java/org/apache/jackrabbit/oak/segment/azure/package-info.java
index e054751151..99e2b3f1cf 100644
--- 
a/oak-segment-azure/src/main/java/org/apache/jackrabbit/oak/segment/azure/package-info.java
+++ 
b/oak-segment-azure/src/main/java/org/apache/jackrabbit/oak/segment/azure/package-info.java
@@ -15,7 +15,7 @@
  * limitations under the License.
  */
 @Internal(since = "1.0.0")
-@Version("2.3.0")
+@Version("2.4.0")
 package org.apache.jackrabbit.oak.segment.azure;
 
 import org.apache.jackrabbit.oak.commons.annotations.Internal;

Reply via email to