ioredis icon indicating copy to clipboard operation
ioredis copied to clipboard

Is credential provider supported?

Open hailin opened this issue 1 year ago • 5 comments

AWS added IAM authentication for ElasticCache Redis cluster. The auth token refreshes every 15 minutes. Is there a way to update client periodically to use updated password for new connections?

Authenticating with IAM

Appreciate any guidance on how to integrate if it's already supported today. If not, is it possible to add support for credential provider?

Thanks!

hailin avatar Mar 29 '23 04:03 hailin

Also very interested in this.

Based on a scan through the source code, it looks like maybe we can handle this by implementing a custom Connector? Except that seems potentially too low level and maybe we actually need hooks inside of the Redis client's connect() method instead.

I'm having a hard time understanding exactly where we could swap in logic to handle an async re-auth (where we could do the temporary credential refresh inside of using the AWS SDK).

knksmith57 avatar Apr 12 '23 23:04 knksmith57

Recently tried a work around approach like this:

const client = new Redis(`rediss://${domain}`, {
            username: 'username',
            password: getAuthToken()
        });

        //generate auth token to authenticate with for each auth command call
        client.auth = new Proxy(this.auth, {
            apply(target, thisArg) {
                return Reflect.apply(target, thisArg, ['username', getAuthToken()]);
            }
        });

So for each auth call that the redis client makes, it generates a valid auth token to use instead of a potentially expired one that you originally passed when instantiating the redis client. And you can cache your auth token for the expiration time of the token if you'd like. Seems to work so far for me without getting any errors.

adamgilbert912 avatar Apr 18 '23 21:04 adamgilbert912

@adamgilbert912 I tried this solution but it's not working for me. Would be able to share more details. Can you please share how you're generating Auth token as well.

chandank-nu avatar Dec 08 '23 06:12 chandank-nu

I'm using Redis.Cluster to connect to elasticache cluster but the proxy auth object is not executing so the server is crashing.

chandank-nu avatar Dec 14 '23 19:12 chandank-nu

Not a proper solution either, but I managed to cobble something together using the connector API. The main issue I had was that the connector has no reference to the Redis instance, but it needs to update the Redis instance's condition property from the connect() function. Because the connector class is passed when creating the Redis instance, and because there's no way to access the actual connector instance (the field is private), some duct tape was needed.

Here's the code (there be dragons):

import { Redis, RedisOptions } from "ioredis";
import StandaloneConnector, { StandaloneConnectionOptions } from "ioredis/built/connectors/StandaloneConnector";
import { ErrorEmitter } from "ioredis/built/connectors/AbstractConnector";
import { NetStream } from "ioredis/built/types";
import ConnectorConstructor from "ioredis/built/connectors/ConnectorConstructor";

/**
 * RedisRef is a reference to a Redis instance. RedisRef.current should be set
 * once the Redis instance is created, and before the first connection happens.
 */
export interface RedisRef {
  /** reference to the Redis instance. */
  current: Redis | null;
}

/**
 * RedisTokenConnectorOptions is an extension of RedisOptions that defines the
 * options for RedisTokenConnector.
*/
export interface RedisTokenConnectorOptions {
  tokenConnector: {
    /**
     * reference to an existing RedisRef instance, which will be used when
     * connecting to access the Redis instance.
     */
    redisRef: RedisRef;

    /**
    * function used to retrieve the connection password. RedisTokenConnector
    * does not implement any kind of credential caching, this should be done by
    * getToken if needed.
    */
    getToken: () => Promise<string>;
  };
}

/**
 * The ActualStandaloneConnector hack below is required because somehow the
 * default import is broken when using ESM modules.
 */
interface StandaloneConnectorConstructor {
  new (opts: StandaloneConnectionOptions): StandaloneConnector;
}

const ActualStandaloneConnector = (StandaloneConnector as unknown as { default: StandaloneConnectorConstructor }).default;

/**
 * RedisTokenConnector is a Redis connector that fetches the password
 * dynamically on each connect() call, and hacks it in the Redis instance.
 */
export class RedisTokenConnector extends ActualStandaloneConnector {
  private redisRef: RedisRef;
  private getToken: () => Promise<string>;

  constructor(options: StandaloneConnectionOptions & RedisTokenConnectorOptions) {
    super(options);

    this.redisRef = options.tokenConnector.redisRef;
    this.getToken = options.tokenConnector.getToken;
  }

  override async connect(_: ErrorEmitter): Promise<NetStream> {
    const token = await this.getToken();

    const condition = this.redisRef.current?.condition;
    if (!condition) throw new Error("expected redis.condition to be set at this point");

    if (condition.auth === undefined || typeof condition.auth === "string") {
      condition.auth = token; // password only
    } else if (Array.isArray(condition.auth)) {
      condition.auth = [condition.auth[0], token]; // [username, password]
    }

    return super.connect(_);
  }
}

and this is how you'd use this:

const redisRef: RedisRef = { current: null };
const redis = new Redis({
  // ... other options
  username: "hello user",
  tokenConnector: {
    redisRef,
    getToken: async () => "hello token!",
  },
  Connector: RedisTokenConnector as ConnectorConstructor, // I could probably get rid of this cast by changing the ctor proto
});
// IMPORTANT, if you do not set redisRef.current you'll get an error when connecting
redisRef.current = redis;

abustany avatar Feb 28 '24 21:02 abustany