spring-projects / spring-data-redis

Provides support to increase developer productivity in Java when using Redis, a key-value store. Uses familiar Spring concepts such as a template classes for core API usage and lightweight repository style data access.
https://spring.io/projects/spring-data-redis/
Apache License 2.0
1.77k stars 1.17k forks source link

RedisConnectionFactory with IAM auth for Elasticache #2769

Closed chandank-nu closed 1 year ago

chandank-nu commented 1 year ago

Hi, We recently switched from Password based Auth to IAM auth for our Elasticache cluster enabled Redis. I was able to supply Sig4 signed request as password and connect to Elasticache. This works well for first 12 hours as IAM auth enabled Elasticache disconnects after 12 hours automatically.

As I created a Custom RedisClusterConfiguration while creating RedisConnectionFactory so was expecting Springboot-data-redis and Lettuce to reconnect automatically but it seems like the password / "Sig4 signed request" that was generated for the first time is being cached somewhere.

Here is my code.... I don't see getPassword() being called while trying to reconnect.. I'm getting WRONG username - password error in Logs.

public class CustomRedisClusterConfiguration extends RedisClusterConfiguration {
    private static final Logger LOG = LoggerFactory.getLogger(CustomRedisClusterConfiguration.class);

    private final String replicationGroupId;

    private final String region;

    private final char[] password;

    public CustomRedisClusterConfiguration(Collection<String> clusterNodes,
                                           String replicationGroupId,
                                           String region,
                                           char[] password)
    {
        super(clusterNodes);
        this.replicationGroupId = replicationGroupId;
        this.region = region;
        this.password = password;
    }

    @NotNull
    @Override
    public RedisPassword getPassword() {
        LOG.info("Get Password called...");
        if (this.password == null || this.password.length == 0 ) {
            LOG.info("Using IAM Authentication mechanism for redis connection");
            //get password from IAM Token Request API

            AwsCredentialsProvider defaultCredentialsProvider =
                    DefaultCredentialsProvider.builder().build();

            //Signed URI as Auth token
            String authToken = ElasticCacheIamAuthUtils.toSignedRequestUri(defaultCredentialsProvider
                            .resolveCredentials(),
                    this.getUsername(),
                    this.region,
                    replicationGroupId);

            LOG.info("Auth Token-1 {}", authToken);
            return RedisPassword.of(authToken);
        } else {
            return RedisPassword.of(this.password);
        }

    }
}

Any thoughts how we can supply new password (generated through code) every time it tries to retry connecting to Redis.

Thanks for taking a look at this.

Thanks, Chandan

mp911de commented 1 year ago

What client are you using? Jedis uses a fixed password while Lettuce provides a Credentials Supplier API that you can use without subclassing any Spring Data Redis utilities.

chandank-nu commented 1 year ago

What client are you using? Jedis uses a fixed password while Lettuce provides a Credentials Supplier API that you can use without subclassing any Spring Data Redis utilities.

@mp911de thanks for your response. We're using Lettuce. How can I use credentials supplier API while creating LettuceConnectionFactory? Kindly suggest.

mp911de commented 1 year ago

You have to provide a RedisCredentialsProviderFactory via LettuceClientConfiguration. That could look like:

class MyCredentialsProviderFactory implements RedisCredentialsProviderFactory {

    @Override
    public RedisCredentialsProvider createCredentialsProvider(RedisConfiguration redisConfiguration) {
        Supplier<RedisCredentials> supplier = …;
        return () -> Mono.fromSupplier(supplier);
    }

    @Override
    public RedisCredentialsProvider createSentinelCredentialsProvider(RedisSentinelConfiguration redisConfiguration) {
        Supplier<RedisCredentials> supplier = …;
        return () -> Mono.fromSupplier(supplier);
    }
}

LettuceClientConfiguration clientConfiguration = LettuceClientConfiguration.builder()
    .redisCredentialsProviderFactory(new MyCredentialsProviderFactory())
    .build();
LettuceConnectionFactory connFactory = new LettuceConnectionFactory(redisConfiguration, clientConfiguration);
chandank-nu commented 1 year ago

@mp911de thanks for sharing the code snippet.. We're currently using springboot 2.7 and RedisCredentialsProviderFactory is not present in 2.7.11

mp911de commented 1 year ago

RedisCredentialsProviderFactory was introduced with version 3 and requires an upgrade to Java 17 and Spring Boot 3.

chandank-nu commented 11 months ago

@mp911de We migrated to Java 17 and Springboot 3.1.5 but still facing the same issue. As per your suggestion I implemented RedisCredentialsProviderFactory but the reconnect attempts are failing. The initial connection is a success but I don't see the LettuceConnectionFactory referring to Custom RedisCredentialProviderFactory while reconnect attempt.

` class IamCredentialsProviderFactory implements RedisCredentialsProviderFactory {

    final String replicationGroupId;
    final String userId;
    final String region;
    final char[] password;

    private static final Logger LOG = LoggerFactory.getLogger(IamCredentialsProviderFactory.class);
    IamCredentialsProviderFactory(String replicationGroupId, String userId, String region, char[] password) {
        this.replicationGroupId = replicationGroupId;
        this.userId = userId;
        this.region = region;
        this.password = password;
    }
    @Override
    public RedisCredentialsProvider createCredentialsProvider(RedisConfiguration redisConfiguration) {
        LOG.info("Inside createCredentialsProvider");
        if (password == null || password.length == 0) {
            AwsCredentialsProvider defaultCredentialsProvider =
                    DefaultCredentialsProvider.create();

            String password = ElasticCacheIamAuthUtils
                    .toSignedRequestUri(defaultCredentialsProvider.resolveCredentials(),
                            userId, region, replicationGroupId);

            LOG.info("Inside createCredentialsProvider, password {}", password);
            Supplier<RedisCredentials> supplier = () -> RedisCredentials.just(userId, password);
            return () -> Mono.fromSupplier(supplier);
        } else {
            return redisConfiguration instanceof RedisConfiguration.WithAuthentication &&
                    ((RedisConfiguration.WithAuthentication)redisConfiguration).getPassword().isPresent() ? RedisCredentialsProvider.from(() -> {
                RedisConfiguration.WithAuthentication withAuthentication = (RedisConfiguration.WithAuthentication)redisConfiguration;
                return RedisCredentials.just(withAuthentication.getUsername(), withAuthentication.getPassword().get());
            }) : () -> Mono.just(RedisCredentialsProviderFactory.AbsentRedisCredentials.ANONYMOUS);

        }

    }`
mp911de commented 11 months ago
String password = ElasticCacheIamAuthUtils
                    .toSignedRequestUri(defaultCredentialsProvider.resolveCredentials(),
                            userId, region, replicationGroupId);

            LOG.info("Inside createCredentialsProvider, password {}", password);
            Supplier<RedisCredentials> supplier = () -> RedisCredentials.just(userId, password);
            return () -> Mono.fromSupplier(supplier);

This captures the password that has been created and each request to provide new credentials returns the same password.

You need to return the password within the Supplier, or better, within the Mono.

chandank-nu commented 11 months ago

Thanks! I'll try this today and confirm