Open meenar-se opened 1 year ago
Hi @meenar-se ,
Thanks for opening this feature request.
I think your request is reasonable, but like RDS signer, this would need to be a handwritten utility (whereas most of the SDK is code generated from the API models of the each service), this means that it will have to be properly designed and implemented in a uniform fashion across all SDKs.
Additionally, feature requests are accepted and implemented based on community engagement (comments, upvotes, or duplicate FRs). This helps us prioritize features in the most impactful way, and use the teams resources wisely.
I will transfer this FR to the cross SDK repo for it to gain traction there. This unfortunately means that it will not get prioritized right away, but it doesn't mean it wont in the future.
Since you marked the "I may be able to implement this feature request" checkbox, I'd encourage you to write your implementation here on this ticket for two reasons:
Thanks again 😄 Ran
Configuration file: runtimeConfig.ts
import { fromNodeProviderChain } from '@aws-sdk/credential-providers';
import { Hash } from '@aws-sdk/hash-node';
import { loadConfig } from '@aws-sdk/node-config-provider';
import { SignerConfig } from './Signer';
/**
* @internal
*/
export const getRuntimeConfig = (config: SignerConfig) : SignerConfig => ({
runtime: 'node',
sha256: config?.sha256 ?? Hash.bind(null, 'sha256'),
credentials: config?.credentials ?? fromNodeProviderChain(),
region: config?.region ?? loadConfig(NODE_REGION_CONFIG_OPTIONS, NODE_REGION_CONFIG_FILE_OPTIONS),
expiresIn: 900,
...config,
} as SignerConfig);
Signer Implementation: Signer.ts
import { SignatureV4 } from '@aws-sdk/signature-v4';
import {
AwsCredentialIdentity,
AwsCredentialIdentityProvider,
ChecksumConstructor,
} from '@aws-sdk/types';
import { formatUrl } from '@aws-sdk/util-format-url';
import { getRuntimeConfig as __getRuntimeConfig } from './runtimeConfig';
import { defaultProvider } from '@aws-sdk/credential-provider-node';
export interface SignerConfig {
/**
* The AWS credentials to sign requests with. Uses the default credential provider chain if not specified.
*/
credentials?: AwsCredentialIdentity | AwsCredentialIdentityProvider
/**
* The hostname of the database to connect to.
*/
hostname: string
/**
* The port number the database is listening on.
*/
port?: number
/**
* The region the database is located in. Uses the region inferred from the runtime if omitted.
*/
region?: string
/**
* The SHA256 hasher constructor to sign the request.
*/
sha256?: ChecksumConstructor
/**
* The username to login as.
*/
username: string
runtime?: string
expiresIn?: number
}
/**
* The signer class that generates an auth token to a database.
*/
export class Signer {
private readonly protocol: string = 'http:';
private readonly service: string = 'elasticache';
public constructor(public configuration: SignerConfig) {
this.configuration = __getRuntimeConfig(configuration);
}
public async getAuthToken(): Promise<string> {
const signer = new SignatureV4({
service: this.service,
region: this.configuration.region!,
credentials: this.configuration.credentials ?? defaultProvider(),
sha256: this.configuration.sha256!,
});
const request = new HttpRequest({
method: 'GET',
protocol: this.protocol,
hostname: this.configuration.hostname,
port: this.configuration.port,
query: {
Action: 'connect',
User: this.configuration.username,
},
headers: {
host: `${this.configuration.hostname}`,
},
});
const presigned = await signer.presign(request, {
expiresIn: this.configuration.expiresIn,
});
const format = formatUrl(presigned).replace(`${this.protocol}//`, '');
console.log(format);
return format;
}
}
Usage example
import { createClient } from "redis";
import { Signer } from "./Signer";
export const redisConnect = async () => {
console.log("calling redis connect");
const credentials = await generateAssumeRoleCreds();
const sign = new Signer({
region: region,
hostname: `${replicationGroupId}`,
username: userId,
credentials: credentials,
});
const presignedUrl = await sign.getAuthToken();
const redisConfig = {
url: `redis://master.xxx.xxx.xxxx.use1.cache.amazonaws.com:6379`,
password: presignedUrl,
username: userId,
socket: {
tls: true,
rejectUnauthorized: false,
},
};
const redisClient = await createClient(redisConfig);
try {
await redisClient.connect();
console.log(await redisClient.get("key"));
} catch (error) {
console.log("Error catched " + error);
}
};
I can also work on the implementing this feature in SDK. We can have the signer as common utility and use it for REDIS and RDS. Please suggest.
HI @meenar-se, did you ever get your sample code to work? I've tried it on Elasticache redis/valkey serverless and non-severless and always get ErrorReply: WRONGPASS invalid username-password pair or user is disabled.
For serverless, it looks like an additional ResourceType=ServerlessCache
query parameter is needed, but I've tried it with and without. Ref: https://docs.aws.amazon.com/AmazonElastiCache/latest/dg/auth-iam.html#auth-iam-Connecting
I got it working using https://github.com/aws-samples/elasticache-iam-auth-demo-app as reference. Here is my updated code including serverless support:
import { fromNodeProviderChain } from '@aws-sdk/credential-providers';
import { NODE_REGION_CONFIG_FILE_OPTIONS, NODE_REGION_CONFIG_OPTIONS } from '@smithy/config-resolver';
import { Hash } from '@smithy/hash-node';
import { loadConfig } from '@smithy/node-config-provider';
import { SignerConfig } from './signer';
/**
* @internal
*/
export const getRuntimeConfig = (config: SignerConfig) => {
return {
runtime: 'node',
sha256: config?.sha256 ?? Hash.bind(null, 'sha256'),
credentials: config?.credentials ?? fromNodeProviderChain(),
region: config?.region ?? loadConfig(NODE_REGION_CONFIG_OPTIONS, NODE_REGION_CONFIG_FILE_OPTIONS),
...config,
};
};
import { formatUrl } from '@aws-sdk/util-format-url';
import { HttpRequest } from '@smithy/protocol-http';
import { SignatureV4 } from '@smithy/signature-v4';
import {
AwsCredentialIdentity,
AwsCredentialIdentityProvider,
ChecksumConstructor,
HashConstructor,
Provider,
} from '@smithy/types';
import { getRuntimeConfig as __getRuntimeConfig } from './runtimeConfig';
export enum ClusterType {
normal = 'normal',
serverless = 'serverless',
}
export interface SignerConfig {
/**
* The AWS credentials to sign requests with. Uses the default credential provider chain if not specified.
*/
credentials?: AwsCredentialIdentity | AwsCredentialIdentityProvider;
/**
* The hostname of the cache to connect to.
*/
cacheName: string;
/**
* The resource type of the cluster.
*/
resourceType: string;
/**
* The region the database is located in. Uses the region inferred from the runtime if omitted.
*/
region?: string;
/**
* The SHA256 hasher constructor to sign the request.
*/
sha256?: ChecksumConstructor | HashConstructor;
/**
* The username to login as.
*/
username: string;
}
/**
* The signer class that generates an auth token to a database.
*/
export class Signer {
private readonly credentials: AwsCredentialIdentity | AwsCredentialIdentityProvider;
private readonly cacheName: string;
private readonly resourceType: string;
private readonly protocol: string = 'https:';
private readonly region: string | Provider<string>;
private readonly service: string = 'elasticache';
private readonly sha256: ChecksumConstructor | HashConstructor;
private readonly username: string;
constructor(configuration: SignerConfig) {
const runtimeConfiguration = __getRuntimeConfig(configuration);
this.credentials = runtimeConfiguration.credentials;
this.cacheName = runtimeConfiguration.cacheName;
this.resourceType = runtimeConfiguration.resourceType;
this.region = runtimeConfiguration.region;
this.sha256 = runtimeConfiguration.sha256;
this.username = runtimeConfiguration.username;
}
public async getAuthToken(): Promise<string> {
const signer = new SignatureV4({
service: this.service,
region: this.region,
credentials: this.credentials,
sha256: this.sha256,
});
const request = new HttpRequest({
method: 'GET',
protocol: this.protocol,
hostname: this.cacheName,
query: {
Action: 'connect',
User: this.username,
// add ResourceType property if serverless
...(this.resourceType === 'serverless' ? { ResourceType: 'ServerlessCache' } : {}),
},
headers: {
host: this.cacheName,
},
});
const presigned = await signer.presign(request, {
expiresIn: 900,
});
return formatUrl(presigned).replace(`${this.protocol}//`, '');
}
}
import { fromNodeProviderChain } from '@aws-sdk/credential-providers';
import { createClient } from 'redis';
import { Signer, ClusterType } from './signer';
const options = {
hostname: 'valkey-serverless.id.serverless.euw2.cache.amazonaws.com',
cacheName: 'valkey-serverless',
region: 'eu-west-2',
clusterType: ClusterType.serverless,
username: 'valkey-serverless-user',
};
export const redisConnect = async () => {
let credentials = fromNodeProviderChain();
console.log('calling redis connect');
const sign = new Signer({
credentials: credentials,
cacheName: options.cacheName,
region: options.region,
resourceType: options.clusterType,
username: options.username,
});
const presignedUrl = await sign.getAuthToken();
const redisConfig = {
url: options.hostname,
password: presignedUrl,
username: options.username,
socket: {
tls: true,
rejectUnauthorized: false,
},
};
const redisClient = createClient(redisConfig);
try {
await redisClient.connect();
console.log(await redisClient.ping());
} catch (error) {
console.log('Error catched ' + error);
}
};
Describe the feature
Need an implementation for Redis signer so that its useful to connect to Elastic cache redis version 7 or above using IAM Authentication.
Use Case
Recently AWS introduced an option to connect to Redis 7 using IAM authentication. But its not directly supported by the library. So we have slightly tweaked the RDS signer to support Redis as well. It will be useful if we add the Redis Signer as well in library itself.
Proposed Solution
No response
Other Information
No response
Acknowledgements
SDK version used
3.350.0
Environment details (OS name and version, etc.)
Macos 12.6.3