Skip to content

Commit 8321f5d

Browse files
authored
Support for DefaultAzureCredential (#3230)
* Support for DefaultAzureCredential * bump redis-authx-core * add integration test to verify DefaultAzureCredential auth support
1 parent 02d129c commit 8321f5d

File tree

2 files changed

+77
-71
lines changed

2 files changed

+77
-71
lines changed

pom.xml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -185,12 +185,12 @@
185185
<dependency>
186186
<groupId>redis.clients.authentication</groupId>
187187
<artifactId>redis-authx-core</artifactId>
188-
<version>0.1.1-beta1</version>
188+
<version>0.1.1-beta2</version>
189189
</dependency>
190190
<dependency>
191191
<groupId>redis.clients.authentication</groupId>
192192
<artifactId>redis-authx-entraid</artifactId>
193-
<version>0.1.1-beta1</version>
193+
<version>0.1.1-beta2</version>
194194
<scope>test</scope>
195195
</dependency>
196196
<!-- Start of core dependencies -->
Lines changed: 75 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,19 @@
11
package io.lettuce.authx;
22

3-
import io.lettuce.core.ClientOptions;
4-
import io.lettuce.core.RedisClient;
5-
import io.lettuce.core.RedisFuture;
6-
import io.lettuce.core.RedisURI;
7-
import io.lettuce.core.SocketOptions;
8-
import io.lettuce.core.TimeoutOptions;
9-
import io.lettuce.core.TransactionResult;
3+
import com.azure.identity.DefaultAzureCredential;
4+
import com.azure.identity.DefaultAzureCredentialBuilder;
5+
import io.lettuce.core.*;
106
import io.lettuce.core.api.StatefulRedisConnection;
117
import io.lettuce.core.api.async.RedisAsyncCommands;
128
import io.lettuce.core.api.sync.RedisCommands;
13-
import io.lettuce.core.cluster.ClusterClientOptions;
149
import io.lettuce.core.pubsub.StatefulRedisPubSubConnection;
1510
import io.lettuce.core.support.PubSubTestListener;
1611
import io.lettuce.test.Wait;
1712
import io.lettuce.test.env.Endpoints;
1813
import io.lettuce.test.env.Endpoints.Endpoint;
19-
import org.junit.jupiter.api.AfterAll;
20-
import org.junit.jupiter.api.Assumptions;
21-
import org.junit.jupiter.api.BeforeAll;
22-
import org.junit.jupiter.api.Tag;
23-
import org.junit.jupiter.api.Test;
14+
import org.junit.jupiter.api.*;
2415
import redis.clients.authentication.core.TokenAuthConfig;
16+
import redis.clients.authentication.entraid.AzureTokenAuthConfigBuilder;
2517
import redis.clients.authentication.entraid.EntraIDTokenAuthConfigBuilder;
2618

2719
import java.time.Duration;
@@ -41,52 +33,49 @@ public class EntraIdIntegrationTests {
4133

4234
private static final EntraIdTestContext testCtx = EntraIdTestContext.DEFAULT;
4335

44-
private static TokenBasedRedisCredentialsProvider credentialsProvider;
36+
private RedisClient client;
4537

46-
private static RedisClient client;
38+
private Endpoint standalone;
4739

48-
private static Endpoint standalone;
40+
private ClientOptions clientOptions;
4941

50-
@BeforeAll
51-
public static void setup() {
42+
private TokenBasedRedisCredentialsProvider credentialsProvider;
43+
44+
@BeforeEach
45+
public void setup() {
5246
standalone = Endpoints.DEFAULT.getEndpoint("standalone-entraid-acl");
53-
if (standalone != null) {
54-
Assumptions.assumeTrue(testCtx.getClientId() != null && testCtx.getClientSecret() != null,
55-
"Skipping EntraID tests. Azure AD credentials not provided!");
56-
// Configure timeout options to assure fast test failover
57-
ClusterClientOptions clientOptions = ClusterClientOptions.builder()
58-
.socketOptions(SocketOptions.builder().connectTimeout(Duration.ofSeconds(1)).build())
59-
.timeoutOptions(TimeoutOptions.enabled(Duration.ofSeconds(1)))
60-
// enable re-authentication
61-
.reauthenticateBehavior(ClientOptions.ReauthenticateBehavior.ON_NEW_CREDENTIALS).build();
62-
63-
TokenAuthConfig tokenAuthConfig = EntraIDTokenAuthConfigBuilder.builder().clientId(testCtx.getClientId())
64-
.secret(testCtx.getClientSecret()).authority(testCtx.getAuthority()).scopes(testCtx.getRedisScopes())
65-
.expirationRefreshRatio(0.0000001F).build();
66-
67-
credentialsProvider = TokenBasedRedisCredentialsProvider.create(tokenAuthConfig);
68-
69-
RedisURI uri = RedisURI.create((standalone.getEndpoints().get(0)));
70-
uri.setCredentialsProvider(credentialsProvider);
71-
client = RedisClient.create(uri);
72-
client.setOptions(clientOptions);
47+
assumeTrue(standalone != null, "Skipping EntraID tests. Redis host with enabled EntraId not provided!");
48+
Assumptions.assumeTrue(testCtx.getClientId() != null && testCtx.getClientSecret() != null,
49+
"Skipping EntraID tests. Azure AD credentials not provided!");
7350

74-
}
51+
clientOptions = ClientOptions.builder()
52+
.socketOptions(SocketOptions.builder().connectTimeout(Duration.ofSeconds(1)).build())
53+
.timeoutOptions(TimeoutOptions.enabled(Duration.ofSeconds(1)))
54+
.reauthenticateBehavior(ClientOptions.ReauthenticateBehavior.ON_NEW_CREDENTIALS).build();
55+
56+
TokenAuthConfig tokenAuthConfig = EntraIDTokenAuthConfigBuilder.builder().clientId(testCtx.getClientId())
57+
.secret(testCtx.getClientSecret()).authority(testCtx.getAuthority()).scopes(testCtx.getRedisScopes())
58+
.expirationRefreshRatio(0.0000001F).build();
59+
60+
TokenBasedRedisCredentialsProvider credentialsProvider = TokenBasedRedisCredentialsProvider.create(tokenAuthConfig);
61+
62+
client = createClient(credentialsProvider);
7563
}
7664

77-
@AfterAll
78-
public static void cleanup() {
65+
@AfterEach
66+
public void cleanUp() {
7967
if (credentialsProvider != null) {
8068
credentialsProvider.close();
8169
}
70+
if (client != null) {
71+
client.shutdown();
72+
}
8273
}
8374

8475
// T.1.1
8576
// Verify authentication using Azure AD with service principals using Redis Standalone client
8677
@Test
8778
public void standaloneWithSecret_azureServicePrincipalIntegrationTest() throws ExecutionException, InterruptedException {
88-
assumeTrue(standalone != null, "Skipping EntraID tests. Redis host with enabled EntraId not provided!");
89-
9079
try (StatefulRedisConnection<String, String> connection = client.connect()) {
9180
RedisCommands<String, String> sync = connection.sync();
9281
String key = UUID.randomUUID().toString();
@@ -102,32 +91,23 @@ public void standaloneWithSecret_azureServicePrincipalIntegrationTest() throws E
10291
// Test that the Redis client is not blocked/interrupted during token renewal.
10392
@Test
10493
public void renewalDuringOperationsTest() throws InterruptedException {
105-
assumeTrue(standalone != null, "Skipping EntraID tests. Redis host with enabled EntraId not provided!");
106-
107-
// Counter to track the number of command cycles
10894
AtomicInteger commandCycleCount = new AtomicInteger(0);
10995

110-
// Start a thread to continuously send Redis commands
11196
Thread commandThread = new Thread(() -> {
11297
try (StatefulRedisConnection<String, String> connection = client.connect()) {
11398
RedisAsyncCommands<String, String> async = connection.async();
11499
for (int i = 1; i <= 10; i++) {
115-
// Start a transaction with SET and INCRBY commands
116-
RedisFuture<String> multi = async.multi();
117-
RedisFuture<String> set = async.set("key", "1");
118-
RedisFuture<Long> incrby = async.incrby("key", 1);
100+
async.multi();
101+
async.set("key", "1");
102+
async.incrby("key", 1);
119103
RedisFuture<TransactionResult> exec = async.exec();
120104
TransactionResult results = exec.get(1, TimeUnit.SECONDS);
121105

122-
// Increment the command cycle count after each execution
123106
commandCycleCount.incrementAndGet();
124107

125-
// Verify the results from EXEC
126-
assertThat(results).hasSize(2); // We expect 2 responses: SET and INCRBY
127-
128-
// Check the response from each command in the transaction
129-
assertThat((String) results.get(0)).isEqualTo("OK"); // SET "key" = "1"
130-
assertThat((Long) results.get(1)).isEqualTo(2L); // INCRBY "key" by 1, expected result is 2
108+
assertThat(results).hasSize(2);
109+
assertThat((String) results.get(0)).isEqualTo("OK");
110+
assertThat((Long) results.get(1)).isEqualTo(2L);
131111
}
132112
} catch (Exception e) {
133113
fail("Command execution failed during token refresh", e);
@@ -136,16 +116,12 @@ public void renewalDuringOperationsTest() throws InterruptedException {
136116

137117
commandThread.start();
138118

139-
CountDownLatch latch = new CountDownLatch(10); // Wait for at least 10 token renewals
140-
141-
credentialsProvider.credentials().subscribe(cred -> {
142-
latch.countDown(); // Signal each renewal as it's received
143-
});
119+
CountDownLatch latch = new CountDownLatch(10); // Wait for at least 10 token renewalss
120+
credentialsProvider.credentials().subscribe(cred -> latch.countDown());
144121

145122
assertThat(latch.await(1, TimeUnit.SECONDS)).isTrue(); // Wait to reach 10 renewals
146123
commandThread.join(); // Wait for the command thread to finish
147124

148-
// Verify that at least 10 command cycles were executed during the test
149125
assertThat(commandCycleCount.get()).isGreaterThanOrEqualTo(10);
150126
}
151127

@@ -162,7 +138,6 @@ public void renewalDuringPubSubOperationsTest() throws InterruptedException {
162138
connectionPubSub.addListener(listener);
163139
connectionPubSub.sync().subscribe("channel");
164140

165-
// Start a thread to continuously send Redis commands
166141
Thread pubsubThread = new Thread(() -> {
167142
for (int i = 1; i <= 100; i++) {
168143
connectionPubSub1.sync().publish("channel", "message");
@@ -172,17 +147,48 @@ public void renewalDuringPubSubOperationsTest() throws InterruptedException {
172147
pubsubThread.start();
173148

174149
CountDownLatch latch = new CountDownLatch(10);
175-
credentialsProvider.credentials().subscribe(cred -> {
176-
latch.countDown();
177-
});
150+
credentialsProvider.credentials().subscribe(cred -> latch.countDown());
178151

179152
assertThat(latch.await(2, TimeUnit.SECONDS)).isTrue(); // Wait for at least 10 token renewals
180153
pubsubThread.join(); // Wait for the pub/sub thread to finish
181154

182-
// Verify that all messages were received
183155
Wait.untilEquals(100, () -> listener.getMessages().size()).waitOrTimeout();
184156
assertThat(listener.getMessages()).allMatch(msg -> msg.equals("message"));
185157
}
186158
}
187159

160+
@Test
161+
public void azureTokenAuthWithDefaultAzureCredentials() throws ExecutionException, InterruptedException {
162+
DefaultAzureCredential credential = new DefaultAzureCredentialBuilder().build();
163+
164+
TokenAuthConfig tokenAuthConfig = AzureTokenAuthConfigBuilder.builder().defaultAzureCredential(credential)
165+
.tokenRequestExecTimeoutInMs(2000).build();
166+
167+
try (RedisClient azureCredClient = createClient(credentialsProvider);
168+
TokenBasedRedisCredentialsProvider credentialsProvider = TokenBasedRedisCredentialsProvider
169+
.create(tokenAuthConfig);) {
170+
RedisCredentials credentials = credentialsProvider.resolveCredentials().block(Duration.ofSeconds(5));
171+
assertThat(credentials).isNotNull();
172+
173+
String key = UUID.randomUUID().toString();
174+
try (StatefulRedisConnection<String, String> connection = azureCredClient.connect()) {
175+
RedisCommands<String, String> sync = connection.sync();
176+
assertThat(sync.aclWhoami()).isEqualTo(credentials.getUsername());
177+
sync.set(key, "value");
178+
assertThat(sync.get(key)).isEqualTo("value");
179+
assertThat(connection.async().get(key).get()).isEqualTo("value");
180+
assertThat(connection.reactive().get(key).block()).isEqualTo("value");
181+
sync.del(key);
182+
}
183+
}
184+
}
185+
186+
private RedisClient createClient(TokenBasedRedisCredentialsProvider credentialsProvider) {
187+
RedisURI uri = RedisURI.create((standalone.getEndpoints().get(0)));
188+
uri.setCredentialsProvider(credentialsProvider);
189+
RedisClient redis = RedisClient.create(uri);
190+
redis.setOptions(clientOptions);
191+
return redis;
192+
}
193+
188194
}

0 commit comments

Comments
 (0)