aws / aws-sdk-php

Official repository of the AWS SDK for PHP (@awsforphp)
http://aws.amazon.com/sdkforphp
Apache License 2.0
6k stars 1.22k forks source link

Uncaught TypeError: Argument 2 passed to Aws\Signature\SignatureV4::signRequest() must be an instance of Aws\Credentials\CredentialsInterface, instance of GuzzleHttp\Promise\Promise given #1650

Closed ErikThiart closed 5 years ago

ErikThiart commented 5 years ago

Seems to have some difficulty between the SDK and Guzzle.

<?php
// Require the Composer autoloader.
require 'vendor/autoload.php';

use Aws\Credentials\CredentialProvider;
use GuzzleHttp\Client;
use GuzzleHttp\Psr7\Request;
use Aws\Signature\SignatureV4;

$request = new Request(
    'POST',
    'https://example.com/api',
    [
        'body' => [
            "type" => "client",
            "action" => "read",
            "limit" => 10
        ]
    ]      
);

$key['credentials'] = array( 
    'key'    => 'AccessKeyId',
    'secret' => 'SecretAccessKey'
);

$credentials = call_user_func(CredentialProvider::defaultProvider(
    $key
));

// Construct a request signer
$region = 'eu-west-1';
$signer = new SignatureV4("execute-api", $region);

// Sign the request
$request = $signer->signRequest($request, $credentials);

// Send the request
try {
    $response = (new Client)->send($request);
    print_r($response);
}
    catch (Exception $exception) {
    $responseBody = $exception->getResponse()->getBody(true);
    echo $responseBody;
}
$response = (new Client)->send($request);

$results = json_decode($response->getBody());

Results in:

Fatal error: Uncaught TypeError: Argument 2 passed to Aws\Signature\SignatureV4::signRequest() must be an instance of Aws\Credentials\CredentialsInterface, instance of GuzzleHttp\Promise\Promise given, called in C:\Users\Thor\app\index.php on line 37 and defined in C:\Users\Thor\app\vendor\aws\aws-sdk-php\src\Signature\SignatureV4.php:78 Stack trace: #0 C:\Users\Thor\app\index.php(37): Aws\Signature\SignatureV4->signRequest(Object(GuzzleHttp\Psr7\Request), Object(GuzzleHttp\Promise\Promise)) #1 {main} thrown in C:\Users\Thor\app\vendor\aws\aws-sdk-php\src\Signature\SignatureV4.php on line 78

diehlaws commented 5 years ago

Thanks for reaching out to us @ErikThiart. Please keep in mind that the default credentials provider looks for credentials in the following locations, and does not take static credentials declared in-code into account.

In addition to this, a credentials provider does not equate to a credentials interface, which is what the signRequest call is looking for in its second argument. Per the CredentialProvider class documentation page:

Credential providers are functions that accept no arguments and return a promise that is fulfilled with an Aws\Credentials\CredentialsInterface or rejected with an Aws\Exception\CredentialsException.

In order to use credentials from the list above via the default credential provider you need to complete the promise returned by the credential provider with wait() as shown below.

$provider = CredentialProvider::defaultProvider();
$credentials = $provider()->wait();

Alternately, if you prefer to keep the credentials in-code, you can declare your Access Key ID and Secret Access Key as separate variables and use those in a new Credentials() call as shown below. This will require using Aws\Credentials\Credentials instead of Aws\Credentials\CredentialProvider

$key = 'AccessKeyId';
$secret = 'SecretAccessKey';
$creds = new Credentials($key, $secret);

In summary, lines 23-30 in your example need to be changed to do one of the following.

  1. Map the specified credentials to a new Credentials interface
  2. Consume credentials from the default provider chain by completing the promise initiated from the specified CredentialProvider
ErikThiart commented 5 years ago

Hey, @diehlaws thanks for the detailed response, that is what I did initially, but something does not add up. I am pretty sure we need to include the session token somewhere

This is my code

<?php
// Require the Composer autoloader.
require 'vendor/autoload.php';

use Aws\Credentials\Credentials;
use GuzzleHttp\Client;
use GuzzleHttp\Psr7\Request;
use Aws\Signature\SignatureV4;

$request = new Request(
    'POST',
    'https://example.com/api',
    [
        'body' => [
            "type" => "client",
            "action" => "read",
            "limit" => 10
        ]
    ]      
);

$key = 'AccessKeyId';
$secret = 'SecretAccessKey';
$credentials = new Credentials($key, $secret);

// Construct a request signer
$region = 'eu-west-1';
$signer = new SignatureV4("execute-api", $region);

// Sign the request
$request = $signer->signRequest($request, $credentials);

// Send the request
try {
    $response = (new Client)->send($request);
    print_r($response);
}
    catch (Exception $exception) {
    $responseBody = $exception->getResponse()->getBody(true);
    echo $responseBody;
}
$response = (new Client)->send($request);

$results = json_decode($response->getBody());

This is the response:

{"message":"The security token included in the request is invalid."}

Fatal error: Uncaught GuzzleHttp\Exception\ClientException: Client error: `POST https://example.com/api` resulted in a `403 Forbidden` response: {"message":"The security token included in the request is invalid."} in C:\Users\Thor\app\vendor\guzzlehttp\guzzle\src\Exception\RequestException.php on line 113

GuzzleHttp\Exception\ClientException: Client error: `POST https://example.com/api` resulted in a `403 Forbidden` response: {"message":"The security token included in the request is invalid."} in C:\Users\Thor\app\vendor\guzzlehttp\guzzle\src\Exception\RequestException.php on line 113

PS: When I do the auth call to get my access key and secret this is what is returned.

{
    "status": "ok",
    "result": {
        "Tokens": {
            "refresh_token": "eyJjdHkiOi..............truncated"
        },
        "Credentials": {
            "AccessKeyId": "ASI..............truncated",
            "SecretAccessKey": "uhmpjnK..............truncated",
            "SessionToken": "FQoGZXIvYXdzEGoaDGZxLmRm..............truncated",
            "Expiration": "2018-10-18T09:28:59+00:00"
        },
        "Access": [
            "access_role_1",
            "access_role_2",
            "access_role_3",
            "access_role_4",
            "access_role_5",
            "access_role_6",
            "access_role_7",
            "access_role_8",
            "access_role_9",
            "access_role_10",
            "access_role_11",
            "access_role_12"
        ]
    }
}

Should we not be using that SessionTokensomewhere in the new Credentials() call?

diehlaws commented 5 years ago

Thanks for the additional information! The error response looks like it is being returned by your API rather than the API Gateway service itself. Typically an error message involving a security token (as opposed to a session token) means the IAM credentials in use are incorrect, or the IAM user that the credentials map to has MFA enabled and the MFA token is not being passed in. To answer your question directly, the session token is generally only required for calls that use temporary credentials such as the credentials resultant from a GetSessionToken call, or when using an assumed role. Calls that use static API keys for an IAM user shouldn't require a session token to successfully authenticate.

Depending on what you're using for authentication with your API, it may require a session token to properly complete authentication - using the sample PetStore API with the AWS_IAM authorization type I was able to issue GET and POST requests using the Access Key ID and Secret Access Key for an IAM user with the appropriate permissions. However, judging by this AWS Forums post, using a Cognito Identity Pool as your authorizer seems to require including the session token in the credential set for requests sent to your API.

The SigV4 signing looks to be happening correctly with your code, so this appears to be more of an issue with the service. If you continue to experience problems authenticating against your API you may want to open a new support case under the API Gateway service.

ErikThiart commented 5 years ago

Alright just to give some feedback in case someone else runs into this issue.

1 you absolutely need to use the SessionToken in the new Credentials() call and then #2 you need to convert the body to JSON I got that bit based on this: __construct ( string $method, string|Psr\Http\Message\UriInterface $uri, array $headers = [],string|null|resource|Psr\Http\Message\StreamInterface $body = null, string $version = '1.1' ) from the Guzzle Request documentation.

So I ended up changing this:

$request = new Request(
    'POST',
    'https://example.com/api',
    [
        'body' => [
            "type" => "client",
            "action" => "read",
            "limit" => 10
        ]
    ]      
);

to this:

$request = new Request(
   'POST',
   'https://example.com/api',
   [],
   '{"type":"client","action":"read","limit":10}'
);

And then added the SessionToken to the credentials call like so: $credentials = new Credentials($key, $secret, $session);

and then it worked.