junit
junit
diff --git a/entraid/src/main/java/redis/clients/authentication/entraid/AzureIdentityProvider.java b/entraid/src/main/java/redis/clients/authentication/entraid/AzureIdentityProvider.java
new file mode 100644
index 0000000..74ec984
--- /dev/null
+++ b/entraid/src/main/java/redis/clients/authentication/entraid/AzureIdentityProvider.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright 2024, Redis Ltd. and Contributors
+ * All rights reserved.
+ *
+ * Licensed under the MIT License.
+ */
+package redis.clients.authentication.entraid;
+
+import java.time.Duration;
+import java.util.ArrayList;
+import java.util.Set;
+import java.util.function.Supplier;
+
+import com.azure.core.credential.AccessToken;
+import com.azure.core.credential.TokenRequestContext;
+import com.azure.identity.DefaultAzureCredential;
+import redis.clients.authentication.core.IdentityProvider;
+import redis.clients.authentication.core.Token;
+
+/**
+ * AzureIdentityProvider is an implementation of the IdentityProvider interface
+ * that uses Azure's DefaultAzureCredential to obtain access tokens.
+ *
+ * This class is designed to work with Azure's identity platform to provide
+ * authentication tokens for accessing Azure resources. It uses a
+ * DefaultAzureCredential to request tokens with specified scopes and a timeout(in milliseconds).
+ * For most cases you will not need to use it directly since AzureTokenAuthConfigBuilder
+ * will do the work for you as shown in the example below:
+ *
+ * {@code
+ * TokenAuthConfig config = AzureTokenAuthConfigBuilder.builder()
+ * .defaultAzureCredential(new DefaultAzureCredential()).build();
+ * }
+ *
+ * In you case you need your own implementation for relevant reasons, you can use it as follows:
+ *
+ * {@code
+ * Set scopes = new HashSet<>(Arrays.asList("https://redis.azure.com/.default"));
+ * AzureIdentityProvider provider = new AzureIdentityProvider(
+ * new DefaultAzureCredentialBuilder().build(), scopes, 5000);
+ * TokenAuthConfig config = AzureTokenAuthConfigBuilder.builder().identityProviderConfig(()-> provider)).build();
+ * }
+ *
+ *
+ * Thread Safety: This class is thread-safe as long as the provided
+ * DefaultAzureCredential is thread-safe.
+ *
+ * @see redis.clients.authentication.entraid.AzureTokenAuthConfigBuilder
+ * @see com.azure.identity.DefaultAzureCredentialBuilder
+ */
+
+public final class AzureIdentityProvider implements IdentityProvider {
+
+ private Supplier accessTokenSupplier;
+
+ public AzureIdentityProvider(DefaultAzureCredential defaultAzureCredential, Set scopes,
+ int timeout) {
+ TokenRequestContext ctx = new TokenRequestContext()
+ .setScopes(new ArrayList(scopes));
+ accessTokenSupplier = () -> defaultAzureCredential.getToken(ctx)
+ .block(Duration.ofMillis(timeout));
+ }
+
+ @Override
+ public Token requestToken() {
+ return new JWToken(accessTokenSupplier.get().getToken());
+ }
+}
diff --git a/entraid/src/main/java/redis/clients/authentication/entraid/AzureIdentityProviderConfig.java b/entraid/src/main/java/redis/clients/authentication/entraid/AzureIdentityProviderConfig.java
new file mode 100644
index 0000000..08110d5
--- /dev/null
+++ b/entraid/src/main/java/redis/clients/authentication/entraid/AzureIdentityProviderConfig.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright 2024, Redis Ltd. and Contributors
+ * All rights reserved.
+ *
+ * Licensed under the MIT License.
+ */
+package redis.clients.authentication.entraid;
+
+import java.util.Set;
+import java.util.function.Supplier;
+
+import com.azure.identity.DefaultAzureCredential;
+
+import redis.clients.authentication.core.IdentityProvider;
+import redis.clients.authentication.core.IdentityProviderConfig;
+
+/**
+ * Configuration class for Azure Identity Provider.
+ * This class implements the {@link IdentityProviderConfig} interface and provides
+ * a configuration for creating an {@link AzureIdentityProvider} instance.
+ *
+ * This class uses {@link DefaultAzureCredential} for authentication and allows
+ * specifying scopes and a timeout(in milliseconds) for the identity provider.
+ * For most cases you will not need to use it directly since AzureTokenAuthConfigBuilder
+ * will do the work for you as shown in the example below:
+ *
+ * {@code
+ * TokenAuthConfig config = AzureTokenAuthConfigBuilder.builder()
+ * .defaultAzureCredential(new DefaultAzureCredentialBuilder()).build();
+ * }
+ *
+ * In you case you need your own implementation for relevant reasons, you can use it as follows:
+ *
+ * {@code
+ * DefaultAzureCredential credential = new DefaultAzureCredentialBuilder().build();
+ * Set scopes = Set.of("https://redis.azure.com/.default");
+ * AzureIdentityProviderConfig azureIDPConfig = new AzureIdentityProviderConfig(credential, scopes, 5000);
+ * TokenAuthConfig config = AzureTokenAuthConfigBuilder.builder().identityProviderConfig(azureIDPConfig).build();
+ * }
+ *
+ *
+ * For more information and details on how to use, please see:
+ * https://github.com/redis/jedis/blob/master/docs/advanced-usage.md#token-based-authentication
+ * https://github.com/redis/lettuce/blob/main/docs/user-guide/connecting-redis.md#microsoft-entra-id-authentication
+ *
+ * @see IdentityProviderConfig
+ * @see AzureIdentityProvider
+ * @see DefaultAzureCredential
+ */
+public final class AzureIdentityProviderConfig implements IdentityProviderConfig {
+
+ private final Supplier providerSupplier;
+
+ public AzureIdentityProviderConfig(DefaultAzureCredential defaultAzureCredential, Set scopes, int timeout) {
+ providerSupplier = () -> new AzureIdentityProvider(defaultAzureCredential, scopes, timeout);
+ }
+
+ @Override
+ public IdentityProvider getProvider() {
+ return providerSupplier.get();
+ }
+}
diff --git a/entraid/src/main/java/redis/clients/authentication/entraid/AzureTokenAuthConfigBuilder.java b/entraid/src/main/java/redis/clients/authentication/entraid/AzureTokenAuthConfigBuilder.java
new file mode 100644
index 0000000..09db995
--- /dev/null
+++ b/entraid/src/main/java/redis/clients/authentication/entraid/AzureTokenAuthConfigBuilder.java
@@ -0,0 +1,118 @@
+/*
+ * Copyright 2024, Redis Ltd. and Contributors
+ * All rights reserved.
+ *
+ * Licensed under the MIT License.
+ */
+package redis.clients.authentication.entraid;
+
+import java.util.Collections;
+import java.util.Set;
+
+import com.azure.identity.DefaultAzureCredential;
+
+import redis.clients.authentication.core.TokenAuthConfig;
+import redis.clients.authentication.core.TokenManagerConfig;
+
+/**
+ * Builder class for configuring Azure Token Authentication via a DefaultAzureCredential.
+ * It builds a TokenAuthConfig object which can be used to authenticate with Azure resources.
+ * This class extends {@link TokenAuthConfig.Builder} and implements {@link AutoCloseable}.
+ * It provides methods to configure various parameters for Azure Token Authentication.
+ *
+ * Default values:
+ *
+ * - {@code DEFAULT_EXPIRATION_REFRESH_RATIO}: 0.75F
+ * - {@code DEFAULT_LOWER_REFRESH_BOUND_MILLIS}: 2 * 60 * 1000
+ * - {@code DEFAULT_TOKEN_REQUEST_EXECUTION_TIMEOUT_IN_MS}: 1000
+ * - {@code DEFAULT_MAX_ATTEMPTS_TO_RETRY}: 5
+ * - {@code DEFAULT_DELAY_IN_MS_TO_RETRY}: 100
+ * - {@code DEFAULT_SCOPES}: "https://redis.azure.com/.default"
+ *
+ *
+ * Example usage:
+ * {@code
+ * AzureTokenAuthConfigBuilder builder = AzureTokenAuthConfigBuilder.builder()
+ * .defaultAzureCredential(new DefaultAzureCredentialBuilder.build())
+ * .scopes(Collections.singleton("https://example.com/.default"))
+ * .tokenRequestExecTimeoutInMs(2000);
+ * TokenAuthConfig config = builder.build();
+ * }
+ *
+ * This class is also {@link AutoCloseable}, and resources can be cleaned
+ * up by calling {@link #close()}.
+ *
+ * @see TokenAuthConfig.Builder
+ * @see DefaultAzureCredential
+ */
+public class AzureTokenAuthConfigBuilder
+ extends TokenAuthConfig.Builder implements AutoCloseable {
+ public static final float DEFAULT_EXPIRATION_REFRESH_RATIO = 0.75F;
+ public static final int DEFAULT_LOWER_REFRESH_BOUND_MILLIS = 2 * 60 * 1000;
+ public static final int DEFAULT_TOKEN_REQUEST_EXECUTION_TIMEOUT_IN_MS = 1000;
+ public static final int DEFAULT_MAX_ATTEMPTS_TO_RETRY = 5;
+ public static final int DEFAULT_DELAY_IN_MS_TO_RETRY = 100;
+ public static final Set DEFAULT_SCOPES = Collections.singleton("https://redis.azure.com/.default");;
+
+ private DefaultAzureCredential defaultAzureCredential;
+ private Set scopes = DEFAULT_SCOPES;
+ private int tokenRequestExecTimeoutInMs = DEFAULT_TOKEN_REQUEST_EXECUTION_TIMEOUT_IN_MS;
+
+ public AzureTokenAuthConfigBuilder() {
+ this.expirationRefreshRatio(DEFAULT_EXPIRATION_REFRESH_RATIO)
+ .lowerRefreshBoundMillis(DEFAULT_LOWER_REFRESH_BOUND_MILLIS)
+ .tokenRequestExecTimeoutInMs(DEFAULT_TOKEN_REQUEST_EXECUTION_TIMEOUT_IN_MS)
+ .maxAttemptsToRetry(DEFAULT_MAX_ATTEMPTS_TO_RETRY)
+ .delayInMsToRetry(DEFAULT_DELAY_IN_MS_TO_RETRY);
+ }
+
+ public AzureTokenAuthConfigBuilder defaultAzureCredential(
+ DefaultAzureCredential defaultAzureCredential) {
+ this.defaultAzureCredential = defaultAzureCredential;
+ return this;
+ }
+
+ public AzureTokenAuthConfigBuilder scopes(Set scopes) {
+ this.scopes = scopes;
+ return this;
+ }
+
+ @Override
+ public AzureTokenAuthConfigBuilder tokenRequestExecTimeoutInMs(
+ int tokenRequestExecTimeoutInMs) {
+ super.tokenRequestExecTimeoutInMs(tokenRequestExecTimeoutInMs);
+ this.tokenRequestExecTimeoutInMs = tokenRequestExecTimeoutInMs;
+ return this;
+ }
+
+ public TokenAuthConfig build() {
+ super.identityProviderConfig(new AzureIdentityProviderConfig(defaultAzureCredential, scopes,
+ tokenRequestExecTimeoutInMs));
+ return super.build();
+ }
+
+ @Override
+ public void close() throws Exception {
+ defaultAzureCredential = null;
+ scopes = null;
+ }
+
+ public static AzureTokenAuthConfigBuilder builder() {
+ return new AzureTokenAuthConfigBuilder();
+ }
+
+ public static AzureTokenAuthConfigBuilder from(AzureTokenAuthConfigBuilder sample) {
+ TokenAuthConfig tokenAuthConfig = TokenAuthConfig.Builder.from(sample).build();
+ TokenManagerConfig tokenManagerConfig = tokenAuthConfig.getTokenManagerConfig();
+
+ AzureTokenAuthConfigBuilder builder = (AzureTokenAuthConfigBuilder) new AzureTokenAuthConfigBuilder()
+ .expirationRefreshRatio(tokenManagerConfig.getExpirationRefreshRatio())
+ .lowerRefreshBoundMillis(tokenManagerConfig.getLowerRefreshBoundMillis())
+ .tokenRequestExecTimeoutInMs(tokenManagerConfig.getTokenRequestExecTimeoutInMs())
+ .maxAttemptsToRetry(tokenManagerConfig.getRetryPolicy().getMaxAttempts())
+ .delayInMsToRetry(tokenManagerConfig.getRetryPolicy().getdelayInMs())
+ .identityProviderConfig(tokenAuthConfig.getIdentityProviderConfig());
+ builder.scopes = sample.scopes;
+ return builder;
+ }
+}
diff --git a/entraid/src/main/java/redis/clients/authentication/entraid/EntraIDTokenAuthConfigBuilder.java b/entraid/src/main/java/redis/clients/authentication/entraid/EntraIDTokenAuthConfigBuilder.java
index 6eeefc4..46942bc 100644
--- a/entraid/src/main/java/redis/clients/authentication/entraid/EntraIDTokenAuthConfigBuilder.java
+++ b/entraid/src/main/java/redis/clients/authentication/entraid/EntraIDTokenAuthConfigBuilder.java
@@ -18,6 +18,60 @@
import redis.clients.authentication.entraid.ManagedIdentityInfo.UserManagedIdentityType;
import redis.clients.authentication.entraid.ServicePrincipalInfo.ServicePrincipalAccess;
+/**
+ * Builder class for configuring EntraID token authentication.
+ * This class provides methods to set various configuration options for EntraID token authentication.
+ * It extends the TokenAuthConfig.Builder class and implements AutoCloseable.
+ *
+ * Default values:
+ *
+ * - DEFAULT_EXPIRATION_REFRESH_RATIO: 0.75F
+ * - DEFAULT_LOWER_REFRESH_BOUND_MILLIS: 120000 (2 minutes)
+ * - DEFAULT_TOKEN_REQUEST_EXECUTION_TIMEOUT_IN_MS: 1000 (1 second)
+ * - DEFAULT_MAX_ATTEMPTS_TO_RETRY: 5
+ * - DEFAULT_DELAY_IN_MS_TO_RETRY: 100 (0.1 second)
+ *
+ *
+ * Configuration options:
+ *
+ * - {@link #clientId(String)}: Sets the client ID.
+ * - {@link #secret(String)}: Sets the client secret and configures access with secret.
+ * - {@link #key(PrivateKey, X509Certificate)}: Sets the private key and certificate,
+ * and configures access with certificate.
+ * - {@link #authority(String)}: Sets the authority URL.
+ * - {@link #systemAssignedManagedIdentity()}: Configures system-assigned managed identity.
+ * - {@link #userAssignedManagedIdentity(UserManagedIdentityType, String)}:
+ * Configures user-assigned managed identity.
+ * - {@link #customEntraIdAuthenticationSupplier(Supplier)}: Sets a custom authentication supplier.
+ * - {@link #scopes(Set)}: Sets the scopes for the token request.
+ * - {@link #tokenRequestExecTimeoutInMs(int)}: Sets the token request execution timeout in milliseconds.
+ *
+ *
+ * Usage:
+ *
+ * {@code
+ * EntraIDTokenAuthConfigBuilder builder = EntraIDTokenAuthConfigBuilder.builder()
+ * .clientId("your-client-id")
+ * .secret("your-secret")
+ * .authority("https://login.microsoftonline.com/your-tenant-id")
+ * .scopes(Set.of("https://redis.azure.com/.default"))
+ * .build();
+ * }
+ *
+ *
+ * Note:
+ *
+ * - Only one of ServicePrincipal, ManagedIdentity or customEntraIdAuthenticationSupplier can be configured.
+ * - Throws RedisEntraIDException if conflicting configurations are provided.
+ *
+ * For more information and details on how to use, please see:
+ * https://github.com/redis/jedis/blob/master/docs/advanced-usage.md#token-based-authentication
+ *
https://github.com/redis/lettuce/blob/main/docs/user-guide/connecting-redis.md#microsoft-entra-id-authentication
+ *
+ * @see TokenAuthConfig.Builder
+ * @see AutoCloseable
+ *
+ */
public class EntraIDTokenAuthConfigBuilder
extends TokenAuthConfig.Builder implements AutoCloseable {
public static final float DEFAULT_EXPIRATION_REFRESH_RATIO = 0.75F;
diff --git a/entraid/src/test/java/redis/clients/authentication/AzureIdentityProviderIntegrationTests.java b/entraid/src/test/java/redis/clients/authentication/AzureIdentityProviderIntegrationTests.java
new file mode 100644
index 0000000..2b62a30
--- /dev/null
+++ b/entraid/src/test/java/redis/clients/authentication/AzureIdentityProviderIntegrationTests.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2024, Redis Ltd. and Contributors
+ * All rights reserved.
+ *
+ * Licensed under the MIT License.
+ */
+package redis.clients.authentication;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import org.junit.Test;
+import com.azure.identity.DefaultAzureCredential;
+import com.azure.identity.DefaultAzureCredentialBuilder;
+
+import redis.clients.authentication.core.Token;
+import redis.clients.authentication.entraid.AzureIdentityProvider;
+import redis.clients.authentication.entraid.AzureTokenAuthConfigBuilder;
+
+public class AzureIdentityProviderIntegrationTests {
+
+ @Test
+ public void requestTokenWithDefaultAzureCredential() {
+ // ensure environment variables are set
+ String client_id = System.getenv(TestContext.AZURE_CLIENT_ID);
+ assertNotNull(client_id);
+ assertFalse(client_id.isEmpty());
+ String clientSecret = System.getenv(TestContext.AZURE_CLIENT_SECRET);
+ assertNotNull(clientSecret);
+ assertFalse(clientSecret.isEmpty());
+ String tenantId = System.getenv("AZURE_TENANT_ID");
+ assertNotNull(tenantId);
+ assertFalse(tenantId.isEmpty());
+
+ DefaultAzureCredential defaultAzureCredential = new DefaultAzureCredentialBuilder().build();
+ Token token = new AzureIdentityProvider(defaultAzureCredential,
+ AzureTokenAuthConfigBuilder.DEFAULT_SCOPES, 2000).requestToken();
+ assertNotNull(token.getValue());
+ }
+}
diff --git a/entraid/src/test/java/redis/clients/authentication/AzureIdentityProviderUnitTests.java b/entraid/src/test/java/redis/clients/authentication/AzureIdentityProviderUnitTests.java
new file mode 100644
index 0000000..c98879c
--- /dev/null
+++ b/entraid/src/test/java/redis/clients/authentication/AzureIdentityProviderUnitTests.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright 2024, Redis Ltd. and Contributors
+ * All rights reserved.
+ *
+ * Licensed under the MIT License.
+ */
+package redis.clients.authentication;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.atLeast;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.mockConstruction;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import java.time.OffsetDateTime;
+import java.util.Date;
+import java.util.Set;
+
+import org.junit.Test;
+import org.mockito.ArgumentCaptor;
+import org.mockito.MockedConstruction;
+
+import com.auth0.jwt.JWT;
+import com.auth0.jwt.algorithms.Algorithm;
+import com.azure.core.credential.AccessToken;
+import com.azure.core.credential.TokenRequestContext;
+import com.azure.identity.DefaultAzureCredential;
+
+import reactor.core.publisher.Mono;
+import redis.clients.authentication.entraid.AzureIdentityProvider;
+import redis.clients.authentication.entraid.AzureIdentityProviderConfig;
+import redis.clients.authentication.entraid.AzureTokenAuthConfigBuilder;
+
+public class AzureIdentityProviderUnitTests {
+ @Test
+ public void testAzureTokenAuthConfigBuilder() {
+ DefaultAzureCredential mockCredential = mock(DefaultAzureCredential.class);
+ Set scopes = AzureTokenAuthConfigBuilder.DEFAULT_SCOPES;
+ int timeout = 2000;
+
+ try (MockedConstruction mockedConstructor = mockConstruction(
+ AzureIdentityProviderConfig.class,
+ (mock, context) -> {
+ assertEquals(mockCredential, context.arguments().get(0));
+ assertEquals(scopes, context.arguments().get(1));
+ assertEquals(timeout, context.arguments().get(2));
+ })) {
+ AzureTokenAuthConfigBuilder.builder().defaultAzureCredential(mockCredential).scopes(scopes)
+ .tokenRequestExecTimeoutInMs(timeout).build();
+ }
+ }
+
+ public void testAzureIdentityProviderConfig() {
+ DefaultAzureCredential mockCredential = mock(DefaultAzureCredential.class);
+ Set scopes = AzureTokenAuthConfigBuilder.DEFAULT_SCOPES;
+ int timeout = 2000;
+
+ try (MockedConstruction mockedConstructor = mockConstruction(
+ AzureIdentityProvider.class,
+ (mock, context) -> {
+ assertEquals(mockCredential, context.arguments().get(0));
+ assertEquals(scopes, context.arguments().get(1));
+ assertEquals(timeout, context.arguments().get(2));
+ })) {
+ new AzureIdentityProviderConfig(mockCredential, scopes, timeout).getProvider();
+ }
+ }
+
+ @Test
+ public void testRequestWithMockCredential() {
+ String token = JWT.create().withExpiresAt(new Date(System.currentTimeMillis()
+ - 1000))
+ .withClaim("oid", "user1").sign(Algorithm.none());
+
+ AccessToken t = new AccessToken(token, OffsetDateTime.now());
+ Mono monoToken = Mono.just(t);
+ DefaultAzureCredential mockCredential = mock(DefaultAzureCredential.class);
+ when(mockCredential.getToken(any(TokenRequestContext.class))).thenReturn(monoToken);
+ new AzureIdentityProviderConfig(mockCredential,
+ AzureTokenAuthConfigBuilder.DEFAULT_SCOPES, 0).getProvider().requestToken();
+
+ ArgumentCaptor argument = ArgumentCaptor.forClass(TokenRequestContext.class);
+
+ verify(mockCredential, atLeast(1)).getToken(argument.capture());
+ AzureTokenAuthConfigBuilder.DEFAULT_SCOPES
+ .forEach((item) -> assertTrue(argument.getValue().getScopes().contains(item)));
+ }
+}
diff --git a/entraid/src/test/java/redis/clients/authentication/TestContext.java b/entraid/src/test/java/redis/clients/authentication/TestContext.java
index 79c7829..070d25f 100644
--- a/entraid/src/test/java/redis/clients/authentication/TestContext.java
+++ b/entraid/src/test/java/redis/clients/authentication/TestContext.java
@@ -19,13 +19,13 @@
public class TestContext {
- private static final String AZURE_CLIENT_ID = "AZURE_CLIENT_ID";
- private static final String AZURE_AUTHORITY = "AZURE_AUTHORITY";
- private static final String AZURE_CLIENT_SECRET = "AZURE_CLIENT_SECRET";
- private static final String AZURE_PRIVATE_KEY = "AZURE_PRIVATE_KEY";
- private static final String AZURE_CERT = "AZURE_CERT";
- private static final String AZURE_REDIS_SCOPES = "AZURE_REDIS_SCOPES";
- private static final String AZURE_USER_ASSIGNED_MANAGED_IDENTITY_CLIENT_ID = "AZURE_USER_ASSIGNED_MANAGED_IDENTITY_CLIENT_ID";
+ public static final String AZURE_CLIENT_ID = "AZURE_CLIENT_ID";
+ public static final String AZURE_AUTHORITY = "AZURE_AUTHORITY";
+ public static final String AZURE_CLIENT_SECRET = "AZURE_CLIENT_SECRET";
+ public static final String AZURE_PRIVATE_KEY = "AZURE_PRIVATE_KEY";
+ public static final String AZURE_CERT = "AZURE_CERT";
+ public static final String AZURE_REDIS_SCOPES = "AZURE_REDIS_SCOPES";
+ public static final String AZURE_USER_ASSIGNED_MANAGED_IDENTITY_CLIENT_ID = "AZURE_USER_ASSIGNED_MANAGED_IDENTITY_CLIENT_ID";
private String clientId;
private String authority;