diff --git a/packages/web/.env.example b/packages/web/.env.example index 1145c15..a877d0d 100644 --- a/packages/web/.env.example +++ b/packages/web/.env.example @@ -23,3 +23,8 @@ AWS_REGION=us-east-1 # Set to 'false' or leave unset for standard auth (local development) # NOTE: IAM auth automatically enables TLS/SSL encryption (required by AWS) USE_IAM_AUTH=false + +# Redis/ElastiCache username for IAM authentication +# Must match an IAM-enabled user created in ElastiCache User Group +# Only used when USE_IAM_AUTH=true +REDIS_USERNAME=tonys-chips-web diff --git a/packages/web/src/index.ts b/packages/web/src/index.ts index 36d6161..f999b5c 100644 --- a/packages/web/src/index.ts +++ b/packages/web/src/index.ts @@ -34,14 +34,29 @@ async function initializeRedisClient(): Promise { const url = new URL(redisUrl); const host = url.hostname; const port = parseInt(url.port) || 6379; - const username = 'tonys-chips-web'; // Must match ElastiCache User + // Username must match an IAM-enabled ElastiCache user + // For Serverless: often 'default' or a custom IAM user created in ElastiCache + // For regular clusters: custom username from ElastiCache User configuration + const username = process.env.REDIS_USERNAME || 'tonys-chips-web'; + + // Detect if this is a serverless cache + const isServerless = host.includes('.serverless.'); console.log('Initializing Valkey client with IAM authentication...'); console.log(`Connecting to: ${host}:${port}`); + console.log(`Cache Type: ${isServerless ? 'Serverless' : 'Regular Cluster'}`); + console.log(`Username: ${username}`); + console.log(`AWS Region: ${awsRegion}`); console.log('TLS encryption: ENABLED (required for IAM auth)'); // Generate initial IAM auth token - const token = await generateIAMAuthToken(host, port, username, awsRegion); + console.log('Generating IAM authentication token...'); + const token = await generateIAMAuthToken(host, port, username, awsRegion, isServerless); + console.log(`Generated token length: ${token.length} characters`); + console.log(`Token preview: ${token.substring(0, 100)}...`); + if (isServerless) { + console.log('Token includes ResourceType=ServerlessCache parameter'); + } // Create Redis client with IAM credentials and TLS // TLS is REQUIRED for IAM authentication with AWS ElastiCache diff --git a/packages/web/src/utils/elasticache-iam-auth.ts b/packages/web/src/utils/elasticache-iam-auth.ts index da86712..07673d5 100644 --- a/packages/web/src/utils/elasticache-iam-auth.ts +++ b/packages/web/src/utils/elasticache-iam-auth.ts @@ -8,8 +8,9 @@ import { Sha256 } from '@aws-crypto/sha256-js'; * * Based on AWS SigV4 signing process: * 1. Creates a pre-signed URL with Action=connect&User=username - * 2. Signs the URL with AWS credentials - * 3. Returns the signed token (without http:// prefix) for use as password + * 2. For Serverless: includes ResourceType=ServerlessCache + * 3. Signs the URL with AWS credentials + * 4. Returns the signed token (without http:// prefix) for use as password * * Tokens are valid for 15 minutes. * @@ -17,18 +18,36 @@ import { Sha256 } from '@aws-crypto/sha256-js'; * @param port - ElastiCache cluster port (typically 6379) * @param username - IAM-enabled ElastiCache username * @param region - AWS region + * @param isServerless - Whether this is a Serverless cache (default: auto-detect from endpoint) * @returns Signed authentication token to use as password */ export async function generateIAMAuthToken( endpoint: string, port: number, username: string, - region: string + region: string, + isServerless?: boolean ): Promise { // Get AWS credentials from the environment (ECS task role in production) const credentialProvider = fromNodeProviderChain(); const credentials = await credentialProvider(); + // Auto-detect if this is a serverless cache based on endpoint + const detectServerless = isServerless !== undefined + ? isServerless + : endpoint.includes('.serverless.'); + + // Create query parameters + const query: Record = { + Action: 'connect', + User: username, + }; + + // Add ResourceType for Serverless caches (REQUIRED) + if (detectServerless) { + query.ResourceType = 'ServerlessCache'; + } + // Create the HTTP request to sign const request = new HttpRequest({ method: 'GET', @@ -36,10 +55,7 @@ export async function generateIAMAuthToken( hostname: endpoint, port, path: '/', - query: { - Action: 'connect', - User: username, - }, + query, headers: { host: `${endpoint}:${port}`, },