Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[ElastiCache] Add Auth Token Generator for ElastiCache IAM authentication #2755

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changes/nextrelease/elasticache-auth-token-generator.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
[
{
"type": "feature",
"category": "ElastiCache",
"description": "Add Aws\\ElastiCache\\AuthTokenGenerator class to generate IAM authentication tokens for ElastiCache for Redis."
}
]
72 changes: 72 additions & 0 deletions src/ElastiCache/AuthTokenGenerator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
<?php

namespace Aws\ElastiCache;

use Aws;
use Aws\Credentials\Credentials;
use Aws\Credentials\CredentialsInterface;
use Aws\Signature\SignatureV4;
use GuzzleHttp\Promise;
use GuzzleHttp\Psr7\Request;
use GuzzleHttp\Psr7\Uri;

/**
* Generates auth tokens for use with IAM authentication.
*/
class AuthTokenGenerator
{
private $credentialProvider;

/**
* The constructor takes an instance of Credentials or a CredentialProvider.
*
* @param callable|Credentials $creds
*/
public function __construct($creds)
{
if ($creds instanceof CredentialsInterface) {
$promise = new Promise\FulfilledPromise($creds);
$this->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);
}
}
125 changes: 125 additions & 0 deletions tests/ElastiCache/AuthTokenGeneratorTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
<?php
namespace Aws\Test\ElastiCache;

use Aws\Credentials\Credentials;
use Aws\ElastiCache\AuthTokenGenerator;
use GuzzleHttp\Promise;
use Yoast\PHPUnitPolyfills\TestCases\TestCase;

/**
* @covers Aws\ElastiCache\AuthTokenGenerator
*/
class AuthTokenGeneratorTest extends TestCase
{
public function testCanCreateAuthTokenWthCredentialInstance()
{
$creds = new Credentials('foo', 'bar', 'baz');
$connect = new AuthTokenGenerator($creds);
$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=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
);
}
}