Skip to content

Add support for DefaultAzureCredentials #14

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Mar 10, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/entraid_integration.yml
Original file line number Diff line number Diff line change
Expand Up @@ -64,3 +64,4 @@ jobs:
AZURE_CERT: ${{secrets.AZURE_CERT}}
AZURE_PRIVATE_KEY: ${{secrets.AZURE_PRIVATE_KEY}}
AZURE_REDIS_SCOPES: ${{secrets.AZURE_REDIS_SCOPES}}
AZURE_TENANT_ID: ${{secrets.AZURE_TENANT_ID}}
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
/*
* Copyright 2024, Redis Ltd. and Contributors All rights reserved. Licensed under the MIT License.
* Copyright 2024, Redis Ltd. and Contributors
* All rights reserved.
*
* Licensed under the MIT License.
*/
package redis.clients.authentication.core;

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
/*
* Copyright 2024, Redis Ltd. and Contributors All rights reserved. Licensed under the MIT License.
* Copyright 2024, Redis Ltd. and Contributors
* All rights reserved.
*
* Licensed under the MIT License.
*/
package redis.clients.authentication.core;

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
/*
* Copyright 2024, Redis Ltd. and Contributors All rights reserved. Licensed under the MIT License.
* Copyright 2024, Redis Ltd. and Contributors
* All rights reserved.
*
* Licensed under the MIT License.
*/
package redis.clients.authentication.core;

Expand Down
5 changes: 5 additions & 0 deletions entraid/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,11 @@
<artifactId>msal4j</artifactId>
<version>1.17.2</version>
</dependency>
<dependency>
<groupId>com.azure</groupId>
<artifactId>azure-identity</artifactId>
<version>1.15.3</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
*
* <p>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:
* <pre>
* {@code
* TokenAuthConfig config = AzureTokenAuthConfigBuilder.builder()
* .defaultAzureCredential(new DefaultAzureCredential()).build();
* }
* </pre>
* <p>In you case you need your own implementation for relevant reasons, you can use it as follows:
* <pre>
* {@code
* Set<String> 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();
* }
* </pre>
*
* <p>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<AccessToken> accessTokenSupplier;

public AzureIdentityProvider(DefaultAzureCredential defaultAzureCredential, Set<String> scopes,
int timeout) {
TokenRequestContext ctx = new TokenRequestContext()
.setScopes(new ArrayList<String>(scopes));
accessTokenSupplier = () -> defaultAzureCredential.getToken(ctx)
.block(Duration.ofMillis(timeout));
}

@Override
public Token requestToken() {
return new JWToken(accessTokenSupplier.get().getToken());
}
}
Original file line number Diff line number Diff line change
@@ -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.
*
* <p>This class uses {@link DefaultAzureCredential} for authentication and allows
* specifying scopes and a timeout(in milliseconds) for the identity provider.</p>
* 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:
* <pre>
* {@code
* TokenAuthConfig config = AzureTokenAuthConfigBuilder.builder()
* .defaultAzureCredential(new DefaultAzureCredentialBuilder()).build();
* }
* </pre>
* <p>In you case you need your own implementation for relevant reasons, you can use it as follows:
* <pre>
* {@code
* DefaultAzureCredential credential = new DefaultAzureCredentialBuilder().build();
* Set<String> scopes = Set.of("https://redis.azure.com/.default");
* AzureIdentityProviderConfig azureIDPConfig = new AzureIdentityProviderConfig(credential, scopes, 5000);
* TokenAuthConfig config = AzureTokenAuthConfigBuilder.builder().identityProviderConfig(azureIDPConfig).build();
* }
* </pre>
*
* 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<IdentityProvider> providerSupplier;

public AzureIdentityProviderConfig(DefaultAzureCredential defaultAzureCredential, Set<String> scopes, int timeout) {
providerSupplier = () -> new AzureIdentityProvider(defaultAzureCredential, scopes, timeout);
}

@Override
public IdentityProvider getProvider() {
return providerSupplier.get();
}
}
Original file line number Diff line number Diff line change
@@ -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.
*
* <p>Default values:</p>
* <ul>
* <li>{@code DEFAULT_EXPIRATION_REFRESH_RATIO}: 0.75F</li>
* <li>{@code DEFAULT_LOWER_REFRESH_BOUND_MILLIS}: 2 * 60 * 1000</li>
* <li>{@code DEFAULT_TOKEN_REQUEST_EXECUTION_TIMEOUT_IN_MS}: 1000</li>
* <li>{@code DEFAULT_MAX_ATTEMPTS_TO_RETRY}: 5</li>
* <li>{@code DEFAULT_DELAY_IN_MS_TO_RETRY}: 100</li>
* <li>{@code DEFAULT_SCOPES}: "https://redis.azure.com/.default"</li>
* </ul>
*
* <p>Example usage:</p>
* <pre>{@code
* AzureTokenAuthConfigBuilder builder = AzureTokenAuthConfigBuilder.builder()
* .defaultAzureCredential(new DefaultAzureCredentialBuilder.build())
* .scopes(Collections.singleton("https://example.com/.default"))
* .tokenRequestExecTimeoutInMs(2000);
* TokenAuthConfig config = builder.build();
* }</pre>
*
* <p>This class is also {@link AutoCloseable}, and resources can be cleaned
* up by calling {@link #close()}.</p>
*
* @see TokenAuthConfig.Builder
* @see DefaultAzureCredential
*/
public class AzureTokenAuthConfigBuilder
extends TokenAuthConfig.Builder<AzureTokenAuthConfigBuilder> 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<String> DEFAULT_SCOPES = Collections.singleton("https://redis.azure.com/.default");;

private DefaultAzureCredential defaultAzureCredential;
private Set<String> 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<String> 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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
* <p>Default values:</p>
* <ul>
* <li>DEFAULT_EXPIRATION_REFRESH_RATIO: 0.75F</li>
* <li>DEFAULT_LOWER_REFRESH_BOUND_MILLIS: 120000 (2 minutes)</li>
* <li>DEFAULT_TOKEN_REQUEST_EXECUTION_TIMEOUT_IN_MS: 1000 (1 second)</li>
* <li>DEFAULT_MAX_ATTEMPTS_TO_RETRY: 5</li>
* <li>DEFAULT_DELAY_IN_MS_TO_RETRY: 100 (0.1 second)</li>
* </ul>
*
* <p>Configuration options:</p>
* <ul>
* <li>{@link #clientId(String)}: Sets the client ID.</li>
* <li>{@link #secret(String)}: Sets the client secret and configures access with secret.</li>
* <li>{@link #key(PrivateKey, X509Certificate)}: Sets the private key and certificate,
* and configures access with certificate.</li>
* <li>{@link #authority(String)}: Sets the authority URL.</li>
* <li>{@link #systemAssignedManagedIdentity()}: Configures system-assigned managed identity.</li>
* <li>{@link #userAssignedManagedIdentity(UserManagedIdentityType, String)}:
* Configures user-assigned managed identity.</li>
* <li>{@link #customEntraIdAuthenticationSupplier(Supplier)}: Sets a custom authentication supplier.</li>
* <li>{@link #scopes(Set)}: Sets the scopes for the token request.</li>
* <li>{@link #tokenRequestExecTimeoutInMs(int)}: Sets the token request execution timeout in milliseconds.</li>
* </ul>
*
* <p>Usage:</p>
* <pre>
* {@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();
* }
* </pre>
*
* <p>Note:</p>
* <ul>
* <li>Only one of ServicePrincipal, ManagedIdentity or customEntraIdAuthenticationSupplier can be configured.</li>
* <li>Throws RedisEntraIDException if conflicting configurations are provided.</li>
* </ul>
* For more information and details on how to use, please see:
* <p>https://github.com/redis/jedis/blob/master/docs/advanced-usage.md#token-based-authentication
* <p>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<EntraIDTokenAuthConfigBuilder> implements AutoCloseable {
public static final float DEFAULT_EXPIRATION_REFRESH_RATIO = 0.75F;
Expand Down
Original file line number Diff line number Diff line change
@@ -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());
}
}
Loading
Loading