diff --git a/.changes/nextrelease/elasticache-auth-token-generator.json b/.changes/nextrelease/elasticache-auth-token-generator.json new file mode 100644 index 0000000000..e29ad90d31 --- /dev/null +++ b/.changes/nextrelease/elasticache-auth-token-generator.json @@ -0,0 +1,7 @@ +[ + { + "type": "feature", + "category": "ElastiCache", + "description": "Add Aws\\ElastiCache\\AuthTokenGenerator class to generate IAM authentication tokens for ElastiCache for Redis." + } +] \ No newline at end of file diff --git a/src/ElastiCache/AuthTokenGenerator.php b/src/ElastiCache/AuthTokenGenerator.php new file mode 100644 index 0000000000..5576c823cf --- /dev/null +++ b/src/ElastiCache/AuthTokenGenerator.php @@ -0,0 +1,72 @@ +credentialProvider = Aws\constantly($promise); + } else { + $this->credentialProvider = $creds; + } + } + + /** + * Create the token for ElastiCache login. + * + * @param string $replication_group_id The replication group id + * @param string $region The region where the ElastiCache cluster is located + * @param string $username The username to login as + * @param int $lifetime The lifetime of the token in minutes + * + * @return string Token generated + */ + public function createToken($replication_group_id, $region, $username, $lifetime = 15) + { + if (! is_numeric($lifetime) || $lifetime > 15 || $lifetime <= 0) { + throw new \InvalidArgumentException( + "Lifetime must be a positive number less than or equal to 15, was {$lifetime}", + null + ); + } + + $uri = new Uri(); + $uri = $uri->withHost($replication_group_id); + $uri = $uri->withPath('/'); + $uri = $uri->withQuery('Action=connect&User='.$username); + + $request = new Request('GET', $uri); + $signer = new SignatureV4('elasticache', $region); + $provider = $this->credentialProvider; + + $url = (string) $signer->presign( + $request, + $provider()->wait(), + '+'.$lifetime.' minutes' + )->getUri(); + + // Remove 2 extra slash from the presigned url result + return substr($url, 2); + } +} diff --git a/tests/ElastiCache/AuthTokenGeneratorTest.php b/tests/ElastiCache/AuthTokenGeneratorTest.php new file mode 100644 index 0000000000..ffd680f78b --- /dev/null +++ b/tests/ElastiCache/AuthTokenGeneratorTest.php @@ -0,0 +1,125 @@ +createToken( + 'my-replication-group', + 'us-west-2', + 'myRedisUser' + ); + + $this->assertStringContainsString('my-replication-group', $token); + $this->assertStringContainsString('us-west-2', $token); + $this->assertStringContainsString('X-Amz-Credential=foo', $token); + $this->assertStringContainsString('X-Amz-Expires=900', $token); + $this->assertStringContainsString('X-Amz-SignedHeaders=host', $token); + $this->assertStringContainsString('User=myRedisUser', $token); + $this->assertStringContainsString('Action=connect', $token); + } + + public function testCanCreateAuthTokenWthCredentialProvider() + { + $accessKeyId = 'AKID'; + $secretKeyId = 'SECRET'; + $provider = function () use ($accessKeyId, $secretKeyId) { + return Promise\Create::promiseFor( + new Credentials($accessKeyId, $secretKeyId) + ); + }; + + $connect = new AuthTokenGenerator($provider); + $token = $connect->createToken( + 'my-replication-group', + 'us-west-2', + 'myRedisUser' + ); + + $this->assertStringContainsString('my-replication-group', $token); + $this->assertStringContainsString('us-west-2', $token); + $this->assertStringContainsString('X-Amz-Credential=AKID', $token); + $this->assertStringContainsString('X-Amz-Expires=900', $token); + $this->assertStringContainsString('X-Amz-SignedHeaders=host', $token); + $this->assertStringContainsString('User=myRedisUser', $token); + $this->assertStringContainsString('Action=connect', $token); + } + + public function lifetimeProvider() + { + return [ + [1], + [14], + ['14'], + [15], + ]; + } + + /** + * @dataProvider lifetimeProvider + * + * @param $lifetime + */ + public function testCanCreateAuthTokenWthNonDefaultLifetime($lifetime) + { + $creds = new Credentials('foo', 'bar', 'baz'); + $connect = new AuthTokenGenerator($creds); + $token = $connect->createToken( + 'my-replication-group', + 'us-west-2', + 'myRedisUser', + $lifetime + ); + $lifetimeInSeconds = $lifetime * 60; + $this->assertStringContainsString('my-replication-group', $token); + $this->assertStringContainsString('us-west-2', $token); + $this->assertStringContainsString('X-Amz-Credential=foo', $token); + $this->assertStringContainsString("X-Amz-Expires={$lifetimeInSeconds}", $token); + $this->assertStringContainsString('X-Amz-SignedHeaders=host', $token); + $this->assertStringContainsString('User=myRedisUser', $token); + $this->assertStringContainsString('Action=connect', $token); + } + + public function lifetimeFailureProvider() + { + return [ + [0], + ['0'], + [''], + [16], + ['16'], + [10000], + [null], + ]; + } + + /** + * @dataProvider lifetimeFailureProvider + * + * @param $lifetime + */ + public function testThrowsExceptionWithInvalidLifetime($lifetime) + { + $this->expectExceptionMessage("Lifetime must be a positive number less than or equal to 15, was"); + $this->expectException(\InvalidArgumentException::class); + $creds = new Credentials('foo', 'bar', 'baz'); + $connect = new AuthTokenGenerator($creds); + $connect->createToken( + 'my-replication-group', + 'us-west-2', + 'myRedisUser', + $lifetime + ); + } +}