Skip to content

Add AWS IAM authorization support #64

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

Merged
merged 1 commit into from Aug 10, 2021
Merged
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
4 changes: 4 additions & 0 deletions .github/workflows/php.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,3 +37,7 @@ jobs:

- name: Run test suite
run: composer run test
env:
AWS_ACCESS_KEY_ID: key
AWS_SECRET_ACCESS_KEY: secret
AWS_SESSION_TOKEN: token
6 changes: 4 additions & 2 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,9 @@
"guzzlehttp/guzzle": "^6.3|^7.0.1"
},
"require-dev": {
"phpunit/phpunit": "^7.5|^8.0",
"codacy/coverage": "^1.4"
"phpunit/phpunit": "^7.5|^8.0|^9.0",
"codacy/coverage": "^1.4",
"aws/aws-sdk-php": "^3.186"
},
"conflict": {
"guzzlehttp/psr7": "< 1.7.0"
Expand All @@ -46,6 +47,7 @@
"test": "phpunit tests/ --whitelist src/ --coverage-clover build/coverage/xml"
},
"suggest": {
"aws/aws-sdk-php": "Move this package to require section to use AWS IAM authorization",
"gmostafa/php-graphql-oqm": "To have object-to-query mapping support"
}
}
15 changes: 15 additions & 0 deletions src/Auth/AuthInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?php

namespace GraphQL\Auth;

use GuzzleHttp\Psr7\Request;

interface AuthInterface
{
/**
* @param Request $request
* @param array $options
* @return Request
*/
public function run(Request $request, array $options = []): Request;
}
62 changes: 62 additions & 0 deletions src/Auth/AwsIamAuth.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
<?php

namespace GraphQL\Auth;

use Aws\Credentials\Credentials;
use Aws\Credentials\CredentialProvider;
use Aws\Signature\SignatureV4;
use GraphQL\Exception\AwsRegionNotSetException;
use GraphQL\Exception\MissingAwsSdkPackageException;
use GuzzleHttp\Psr7\Request;

class AwsIamAuth implements AuthInterface
{
protected const SERVICE_NAME = 'appsync';

/**
* @codeCoverageIgnore
*
* AwsIamAuth constructor.
*/
public function __construct()
{
if (!class_exists('\Aws\Signature\SignatureV4')) {
throw new MissingAwsSdkPackageException();
}
}

/**
* @param Request $request
* @param array $options
* @return Request
*/
public function run(Request $request, array $options = []): Request
{
$region = $options['aws_region'] ?? null;
if ($region === null) {
throw new AwsRegionNotSetException();
}
return $this->getSignature($region)->signRequest(
$request, $this->getCredentials(),
self::SERVICE_NAME
);
}

/**
* @param string $region
* @return SignatureV4
*/
protected function getSignature(string $region): SignatureV4
{
return new SignatureV4(self::SERVICE_NAME, $region);
}

/**
* @return Credentials
*/
protected function getCredentials(): Credentials
{
$provider = CredentialProvider::defaultProvider();
return $provider()->wait();
}
}
27 changes: 24 additions & 3 deletions src/Client.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

namespace GraphQL;

use GraphQL\Auth\AuthInterface;
use GraphQL\Auth\HeaderAuth;
use GraphQL\Exception\QueryError;
use GraphQL\Exception\MethodNotSupportedException;
use GraphQL\QueryBuilder\QueryBuilderInterface;
Expand Down Expand Up @@ -34,39 +36,54 @@ class Client
*/
protected $httpHeaders;

/**
* @var array
*/
protected $options;

/**
* @var string
*/
protected $requestMethod;

/**
* @var AuthInterface
*/
protected $auth;

/**
* Client constructor.
*
* @param string $endpointUrl
* @param array $authorizationHeaders
* @param array $httpOptions
* @param ClientInterface $httpClient
* @param ClientInterface|null $httpClient
* @param string $requestMethod
* @param AuthInterface|null $auth
*/
public function __construct(
string $endpointUrl,
array $authorizationHeaders = [],
array $httpOptions = [],
ClientInterface $httpClient = null,
string $requestMethod = 'POST'
string $requestMethod = 'POST',
AuthInterface $auth = null
) {
$headers = array_merge(
$authorizationHeaders,
$httpOptions['headers'] ?? [],
['Content-Type' => 'application/json']
);

/**
* All headers will be set on the request objects explicitly,
* Guzzle doesn't have to care about them at this point, so to avoid any conflicts
* we are removing the headers from the options
*/
unset($httpOptions['headers']);
$this->options = $httpOptions;
if ($auth) {
$this->auth = $auth;
}

$this->endpointUrl = $endpointUrl;
$this->httpClient = $httpClient ?? new GuzzleAdapter(new \GuzzleHttp\Client($httpOptions));
Expand Down Expand Up @@ -121,6 +138,10 @@ public function runRawQuery(string $queryString, $resultsAsArray = false, array
$bodyArray = ['query' => (string) $queryString, 'variables' => $variables];
$request = $request->withBody(Utils::streamFor(json_encode($bodyArray)));

if ($this->auth) {
$request = $this->auth->run($request, $this->options);
}

// Send api request and get response
try {
$response = $this->httpClient->sendRequest($request);
Expand Down
18 changes: 18 additions & 0 deletions src/Exception/AwsRegionNotSetException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?php

namespace GraphQL\Exception;

use RunTimeException;

/**
* Class AwsRegionNotSetException
*
* @package GraphQL\Exception
*/
class AwsRegionNotSetException extends RunTimeException
{
public function __construct()
{
parent::__construct("AWS region not set.");
}
}
26 changes: 26 additions & 0 deletions src/Exception/MissingAwsSdkPackageException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?php

namespace GraphQL\Exception;

use RunTimeException;

/**
* Class MissingAwsSdkPackageException
*
* @package GraphQL\Exception
*/
class MissingAwsSdkPackageException extends RunTimeException
{
/**
* @codeCoverageIgnore
*
* MissingAwsSdkPackageException constructor.
*/
public function __construct()
{
parent::__construct(
'To be able to use AWS IAM authorization you should
install "aws/aws-sdk-php" as a project dependency.'
);
}
}
49 changes: 49 additions & 0 deletions tests/Auth/AwsIamAuthTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
<?php

namespace GraphQL\Tests\Auth;

use GraphQL\Auth\AwsIamAuth;
use GraphQL\Exception\AwsRegionNotSetException;
use GuzzleHttp\Psr7\Request;
use PHPUnit\Framework\TestCase;

class AwsIamAuthTest extends TestCase
{
/**
* @var AwsIamAuth
*/
protected $auth;

protected function setUp(): void
{
$this->auth = new AwsIamAuth();
}

/**
* @covers \GraphQL\Auth\AwsIamAuth::run
* @covers \GraphQL\Exception\AwsRegionNotSetException::__construct
*/
public function testRunMissingRegion()
{
$this->expectException(AwsRegionNotSetException::class);
$request = new Request('POST', '');
$this->auth->run($request, []);
}

/**
* @covers \GraphQL\Auth\AwsIamAuth::run
* @covers \GraphQL\Auth\AwsIamAuth::getSignature
* @covers \GraphQL\Auth\AwsIamAuth::getCredentials
*/
public function testRunSuccess()
{
$request = $this->auth->run(
new Request('POST', ''),
['aws_region' => 'us-east-1']
);
$headers = $request->getHeaders();
$this->assertArrayHasKey('X-Amz-Date', $headers);
$this->assertArrayHasKey('X-Amz-Security-Token', $headers);
$this->assertArrayHasKey('Authorization', $headers);
}
}