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 <[email protected]>
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;