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;