diff --git a/composer.json b/composer.json index 86d7e7f..d1d6b89 100644 --- a/composer.json +++ b/composer.json @@ -23,7 +23,8 @@ "psr/cache": "^1.0|^2.0|^3.0", "psr/log": "^1.0|^2.0|^3.0", "psr/http-client": "^1.0", - "psr/http-factory": "^1.0" + "psr/http-factory": "^1.0", + "aws/aws-sdk-php": "^3.0" }, "require-dev": { "codeception/codeception": "^4.1", diff --git a/src/AuthenticationStrategies/AwsIamAuthenticationStrategy.php b/src/AuthenticationStrategies/AwsIamAuthenticationStrategy.php new file mode 100644 index 0000000..0f8274b --- /dev/null +++ b/src/AuthenticationStrategies/AwsIamAuthenticationStrategy.php @@ -0,0 +1,120 @@ +role = $role; + $this->region = $region; + $this->serverId = $vaultServerId; + $this->stsClient = $stsClient; + } + + /** + * Returns auth for further interactions with Vault. + * + * @return Auth + * @throws AuthenticationException + * @throws ClientExceptionInterface + */ + public function authenticate(): Auth + { + if (!$this->methodPathSegment) { + throw new AuthenticationException('methodPathSegment must be set before usage'); + } + + if (!$this->stsClient) { + $this->stsClient = new StsClient([ + 'region' => $this->region, + 'version' => 'latest', + 'sts_regional_endpoints' => 'regional', + ]); + } + + + // Creating a signed command, to get the parameters for the actual login-request to vault + $command = $this->stsClient->getCommand('GetCallerIdentity'); + + if ($this->serverId) { + $command->getHandlerList()->appendBuild( + Middleware::mapRequest(function (RequestInterface $request) { + return $request->withHeader('X-Vault-AWS-IAM-Server-ID', $this->serverId); + }), + 'add-header' + ); + } + + $request = \Aws\serialize($command); + + $response = $this->client->write( + sprintf('/auth/%s/login', $this->methodPathSegment), + [ + 'role' => $this->role, + 'iam_http_request_method' => $request->getMethod(), + 'iam_request_url' => base64_encode($request->getUri()), + 'iam_request_body' => base64_encode($request->getBody()), + 'iam_request_headers' => base64_encode(json_encode($request->getHeaders())), + ] + ); + + return $response->getAuth(); + } + + /** + * @return string + */ + public function getMethodPathSegment(): string + { + return $this->methodPathSegment; + } + + /** + * @param string $methodPathSegment + * + * @return static + */ + public function setMethodPathSegment(string $methodPathSegment) + { + $this->methodPathSegment = $methodPathSegment; + + return $this; + } +} \ No newline at end of file diff --git a/tests/_data/vcr/authentication-strategies/aws-iam b/tests/_data/vcr/authentication-strategies/aws-iam new file mode 100644 index 0000000..7881a10 --- /dev/null +++ b/tests/_data/vcr/authentication-strategies/aws-iam @@ -0,0 +1,46 @@ + +- + request: + method: POST + url: 'http://127.0.0.1:8200/v1/auth/aws/login' + headers: + Host: '127.0.0.1:8200' + Expect: null + Accept-Encoding: null + User-Agent: VaultPHP/1.0.0 + Content-Type: application/json + Accept: null + body: '{"role":"dev-role","iam_http_request_method":"POST","iam_request_url":"aHR0cHM6Ly9zdHMuZXUtY2VudHJhbC0xLmFtYXpvbmF3cy5jb20=","iam_request_body":"QWN0aW9uPUdldENhbGxlcklkZW50aXR5JlZlcnNpb249MjAxMS0wNi0xNQ==","iam_request_headers":"eyJIb3N0IjpbInN0cy5ldS1jZW50cmFsLTEuYW1hem9uYXdzLmNvbSJdLCJDb250ZW50LUxlbmd0aCI6WyI0MyJdLCJDb250ZW50LVR5cGUiOlsiYXBwbGljYXRpb25cL3gtd3d3LWZvcm0tdXJsZW5jb2RlZCJdLCJYLUFtei1Vc2VyLUFnZW50IjpbImF3cy1zZGstcGhwXC8zLjI2MC4zIE9TXC9EYXJ3aW5cLzIyLjEuMCBsYW5nXC9waHBcLzguMC4yNyJdLCJVc2VyLUFnZW50IjpbImF3cy1zZGstcGhwXC8zLjI2MC4zIE9TXC9EYXJ3aW5cLzIyLjEuMCBsYW5nXC9waHBcLzguMC4yNyJdLCJYLVZhdWx0LUFXUy1JQU0tU2VydmVyLUlEIjpbImxvY2FsaG9zdCJdLCJhd3Mtc2RrLXJldHJ5IjpbIjBcLzAiXX0="}' + response: + status: + http_version: '1.1' + code: '200' + message: OK + headers: + Cache-Control: no-store + Content-Type: application/json + Date: 'Tue, 01 Aug 2017 06:54:33 GMT' + Content-Length: '366' + body: "{\"request_id\":\"08962062-3aab-fd89-3413-99c3521ecc75\",\"lease_id\":\"\",\"renewable\":false,\"lease_duration\":0,\"data\":null,\"wrap_info\":null,\"warnings\":null,\"auth\":{\"client_token\":\"9e64c3b8-01f7-7a64-1575-30a91f7d1ae4\",\"accessor\":\"033ccada-d332-9415-0acc-0ec51e81dbd1\",\"policies\":[\"default\",\"test\"],\"metadata\":{\"username\":\"test\"},\"lease_duration\":2764800,\"renewable\":true}}\n" +- + request: + method: GET + url: 'http://127.0.0.1:8200/v1/auth/token/lookup-self' + headers: + Host: '127.0.0.1:8200' + Accept-Encoding: null + User-Agent: VaultPHP/1.0.0 + Content-Type: application/json + X-Vault-Token: 9e64c3b8-01f7-7a64-1575-30a91f7d1ae4 + Accept: null + response: + status: + http_version: '1.1' + code: '200' + message: OK + headers: + Cache-Control: no-store + Content-Type: application/json + Date: 'Tue, 01 Aug 2017 06:54:33 GMT' + Content-Length: '504' + body: "{\"request_id\":\"ab3f3dc9-3633-0c18-44e2-f3f58cb56d0a\",\"lease_id\":\"\",\"renewable\":false,\"lease_duration\":0,\"data\":{\"accessor\":\"033ccada-d332-9415-0acc-0ec51e81dbd1\",\"creation_time\":1501570473,\"creation_ttl\":2764800,\"display_name\":\"userpass-test\",\"explicit_max_ttl\":0,\"id\":\"9e64c3b8-01f7-7a64-1575-30a91f7d1ae4\",\"meta\":{\"username\":\"test\"},\"num_uses\":0,\"orphan\":true,\"path\":\"auth/userpass/login/test\",\"policies\":[\"default\",\"test\"],\"renewable\":true,\"ttl\":2764800},\"wrap_info\":null,\"warnings\":null,\"auth\":null}\n" diff --git a/tests/unit/AuthenticationStrategies/AwsIamAuthenticationStrategyTest.php b/tests/unit/AuthenticationStrategies/AwsIamAuthenticationStrategyTest.php new file mode 100644 index 0000000..782653e --- /dev/null +++ b/tests/unit/AuthenticationStrategies/AwsIamAuthenticationStrategyTest.php @@ -0,0 +1,87 @@ + 'eu-central-1', + 'version' => 'latest', + 'sts_regional_endpoints' => 'regional' + ]); + // These middlewares would break the test, due to some dynamic headers + $stsClient->getHandlerList()->remove('invocation-id'); + $stsClient->getHandlerList()->remove('signer'); + + $client->setAuthenticationStrategy( + new AwsIamAuthenticationStrategy( + 'dev-role', + 'eu-central-1', + 'localhost', + $stsClient + ) + ); + + $this->assertEquals($client->getAuthenticationStrategy()->getClient(), $client); + $this->assertTrue($client->authenticate()); + $this->assertNotEmpty($client->getToken()); + $this->assertNotEmpty($client->getToken()->getAuth()->getLeaseDuration()); + $this->assertNotEmpty($client->getToken()->getAuth()->isRenewable()); + } + + protected function setUp(): void + { + VCR::turnOn(); + VCR::configure()->setMode(VCR::MODE_ONCE); + + VCR::insertCassette('authentication-strategies/aws-iam'); + + parent::setUp(); + } + + protected function tearDown(): void + { + // To stop recording requests, eject the cassette + VCR::eject(); + + // Turn off VCR to stop intercepting requests + VCR::turnOff(); + + parent::tearDown(); + } + + protected function _before() + { + } + + protected function _after() + { + } +}