amzn / selling-partner-api-models

This repository contains OpenAPI models for developers to use when developing software to call Selling Partner APIs.
Apache License 2.0
580 stars 730 forks source link

Java - cannot get RDT, response header has InvalidSignatureException #2236

Closed Mike-the-one closed 2 years ago

Mike-the-one commented 2 years ago

I am able to call getOrder API to get a list of orders but I am unable to get RDT because I need to access buyerInfo and shipping address.

I have followed this sample code here:

https://github.com/amzn/selling-partner-api-models/blob/main/clients/sample-code/RestrictedDataTokenWorkflow.java

But I always get 403 error when I try to get the RDT. This is the code:

CreateRestrictedDataTokenRequest restrictedDataTokenRequest = new CreateRestrictedDataTokenRequest();
        restrictedDataTokenRequest.setTargetApplication("amzn1.sellerapps.app.target-application");

        // Add a resource list to the CreateRestrictedDataTokenRequest object.
        String resourcePath = "/orders/v0/orders";
        // Define the dataElements to indicate the type of Personally Identifiable
        // Information requested.
        // This parameter is required only when getting an RDT for use with the
        // getOrder, getOrders, or getOrderItems operation of the Orders API.

        final List<String> dataElements = Arrays.asList("buyerInfo", "shippingAddress");

        RestrictedResource resource = new RestrictedResource();
        resource.setMethod(RestrictedResource.MethodEnum.GET);
        resource.setPath(resourcePath);
        resource.setDataElements(dataElements);

        List<RestrictedResource> resourceList = Arrays.asList(resource);

        restrictedDataTokenRequest.setRestrictedResources(resourceList);

        TokensApi tokensApi = new TokensApi.Builder().awsAuthenticationCredentials(awsAuthenticationCredentials)
                .awsAuthenticationCredentialsProvider(awsAuthenticationCredentialsProvider)
                .lwaAuthorizationCredentials(lwaAuthorizationCredentials)
                .endpoint("https://sellingpartnerapi-na.amazon.com").build();
        try {
            // Exception when createRestrictedDataToken is called
            CreateRestrictedDataTokenResponse response = tokensApi
                    .createRestrictedDataToken(restrictedDataTokenRequest);
            String restrictedDataToken = response.getRestrictedDataToken();
            return restrictedDataToken;
        } catch (ApiException e) {
            System.out.println(e.getResponseHeaders()); // Capture the response headers when a exception is thrown.
            throw e;
        }

And this is the response header:

Content-Type=[application[/json]()], Content-Length=[1646], Connection=[keep-alive], x-amzn-RequestId=[24346030-3074-450d-9a35-10cf6acb2c1f], x-amzn-ErrorType=[InvalidSignatureException], x-amz-apigw-id=xxxxxxxxx], OkHttp-Sent-Millis=[1643895557395], OkHttp-Received-Millis=[1643895557462]}

awsAuthenticationCredentials, awsAuthenticationCredentialsProvider and lwaAuthorizationCredentials should be correct since I am able to call other endpoints (not restricted resources).

The code was generated using the following command:

java -jar ./swagger-codegen-cli-2.4.13.jar generate -i <model file> --model-package com.amazon.spapi.models.$model -l java -t ../clients/sellingpartner-api-aa-java/resources/swagger-codegen/templates/ -o out -c ./config.json

and config.json:

{
  "groupId": "com.amazon",
  "artifactId": "spapi",
  "artifactVersion": "1.1.0",
  "invokerPackage": "com.amazon.spapi.client",
  "apiPackage": "com.amazon.spapi.api",
  "useGzipFeature": true,
  "library": "okhttp-gson"
}

Thanks!

Mike-the-one commented 2 years ago

Here are the request header and body:

Request body:

{
   "targetApplication":"Target Application",
   "restrictedResources":[
      {
         "method":"GET",
         "path":"[/orders/v0/orders]()",
         "dataElements":[
            "buyerInfo",
            "shippingAddress"
         ]
      }
   ]
}

And headers: (* are the values I replaced)

Accept = application/json
Authorization = AWS4-HMAC-SHA256 Credential=********************/20220204/us-east-1/execute-api/aws4_request, SignedHeaders=accept;content-type;host;user-agent;x-amz-access-token;x-amz-date;x-amz-security-token, Signature=ea0d96571eef2558959beb43c4c062877e073bb4a9f8ab80b2c2443fd8c34f2c
Content-Type = application/json
Host = sellingpartnerapi-na.amazon.com
User-Agent = Swagger-Codegen/1.1.0/java
x-amz-access-token = *************
X-Amz-Date = 20220204T113050Z
X-Amz-Security-Token = ************

Do they look right? The AppClient.java is generated by swagger cli, AWSSigV4Signer.java is from the latest one from repo https://github.com/amzn/selling-partner-api-models

AWS dependency version

compile group: 'com.amazonaws', name: 'aws-java-sdk-signer', version: '1.12.9'
compile group: 'com.amazonaws', name: 'aws-java-sdk-sts', version: '1.12.9'
Mike-the-one commented 2 years ago

This is the buildRequest method from AppClient.java (I added systemout):

/**
     * Build an HTTP request with the given options.
     *
     * @param path                    The sub-path of the HTTP URL
     * @param method                  The request method, one of "GET", "HEAD",
     *                                "OPTIONS", "POST", "PUT", "PATCH" and "DELETE"
     * @param queryParams             The query parameters
     * @param collectionQueryParams   The collection query parameters
     * @param body                    The request body object
     * @param headerParams            The header parameters
     * @param formParams              The form parameters
     * @param authNames               The authentications to apply
     * @param progressRequestListener Progress request listener
     * @return The HTTP request
     * @throws ApiException If fail to serialize the request body object
     */
    public Request buildRequest(String path, String method, List<Pair> queryParams, List<Pair> collectionQueryParams,
            Object body, Map<String, String> headerParams, Map<String, Object> formParams, String[] authNames,
            ProgressRequestBody.ProgressRequestListener progressRequestListener) throws ApiException {
        updateParamsForAuth(authNames, queryParams, headerParams);

        final String url = buildUrl(path, queryParams, collectionQueryParams);
        final Request.Builder reqBuilder = new Request.Builder().url(url);
        processHeaderParams(headerParams, reqBuilder);

        String contentType = (String) headerParams.get("Content-Type");
        // ensuring a default content type
        if (contentType == null) {
            contentType = "application/json";
        }

        RequestBody reqBody;
        if (!HttpMethod.permitsRequestBody(method)) {
            reqBody = null;
        } else if ("application/x-www-form-urlencoded".equals(contentType)) {
            reqBody = buildRequestBodyFormEncoding(formParams);
        } else if ("multipart/form-data".equals(contentType)) {
            reqBody = buildRequestBodyMultipart(formParams);
        } else if (body == null) {
            if ("DELETE".equals(method)) {
                // allow calling DELETE without sending a request body
                reqBody = null;
            } else {
                // use an empty request body (for POST, PUT and PATCH)
                reqBody = RequestBody.create(MediaType.parse(contentType), "");
            }
        } else {
            reqBody = serialize(body, contentType);
        }

        Request request = null;

        if (progressRequestListener != null && reqBody != null) {
            ProgressRequestBody progressRequestBody = new ProgressRequestBody(reqBody, progressRequestListener);
            request = reqBuilder.method(method, progressRequestBody).build();
        } else {
            request = reqBuilder.method(method, reqBody).build();
        }

        request = lwaAuthorizationSigner.sign(request);
        request = awsSigV4Signer.sign(request);

        Headers headers = request.headers();
        Set<String> names = headers.names();
        for (String n : names) {
            System.out.println(n + " = " + headers.get(n));
        }

        return request;
    }
Mike-the-one commented 2 years ago

Tried latest dependencies, same result.

compile group: 'com.amazonaws', name: 'aws-java-sdk-signer', version: '1.12.152'
compile group: 'com.amazonaws', name: 'aws-java-sdk-sts', version: '1.12.152'

I am able to call SellersAPI.getMarketplaceParticipations() and OrdersAPI.getOrders(), both of these are GET commands, so no request body, I wondering if I miss something that the signer does not include the body to sign?

Mike-the-one commented 2 years ago

@ShivikaK Do you mind take a look?

Mike-the-one commented 2 years ago

@ShivikaK

This is the response body (* are the values I replaced)


{
  "errors": [
    {
      "message": "The request signature we calculated does not match the signature you provided. Check your AWS Secret Access Key and signing method. Consult the service documentation for details.

The Canonical String for this request should have been
'POST
/tokens/2021-03-01/restrictedDataToken

accept:application/json
content-type:application/json; charset=utf-8
host:sellingpartnerapi-na.amazon.com
user-agent:Swagger-Codegen/1.1.0/java
x-amz-access-token:**************
x-amz-date:20220204T123510Z
x-amz-security-token:********

accept;content-type;host;user-agent;x-amz-access-token;x-amz-date;x-amz-security-token
********(I replaced)'

The String-to-Sign should have been
'AWS4-HMAC-SHA256
20220204T123510Z
20220204/us-east-1/execute-api/aws4_request
0fa************************************8e2c'
",
     "code": "InvalidSignature"
    }
  ]
}
Mike-the-one commented 2 years ago

For anyone had the similar issue, the problem is how I generate the client code, especially this line

"useGzipFeature": true,

If turned on, the ApiClient.java will have a line like this

httpClient.interceptors().add(new GzipRequestInterceptor());

That is the problem. The signature was added before the gzip header is added, then Amazon will calculate the signature with the gzip and of course it will not match!

Thanks to @rohitdobariya who spotted the issue!