Flutter use Amazon Product API

98 Views Asked by At

I am trying to use the Amazon Product API, but keep getting this error:

{__type: com.amazon.paapi5#InvalidSignatureException, Errors: [{Code: InvalidSignature, Message: The request has not been correctly signed. If you are using an AWS SDK, requests are signed for you automatically; otherwise, go to https://webservices.amazon.com/paapi5/documentation/sending-request.html#signing.}]}

I am using this repo.

The Service Class I am using looks like this:

class PaAPI {
  static const awsHmacAlgorithm = 'AWS4-HMAC-SHA256';
  static const aws4Request = 'aws4_request';

  /// Amazon Access Key
  String accessKey;

  /// Amazon Secret Key
  String secretKey;

  /// Service name
  String service = 'ProductAdvertisingAPI';

  /// Host
  late String host;

  /// Region
  late String region;

  /// Markeptlace default=us
  PaAPIMarketplace marketplace;

  /// API Request PATH
  late String path;

  /// Amazon associate tag
  late String partnerTag;

  PaAPI(
      {required this.accessKey,
      required this.secretKey,
      required this.partnerTag,
      this.marketplace = PaAPIMarketplace.us}) {
    this.host = this.marketplace.host;
    this.region = this.marketplace.region;
  }

  Future<GetItemsResponse> getItems(
      List<String> items, List<String> resources) async {
    final body = {
      "ItemIds": items,
      "Resources": resources,
      "PartnerTag": this.partnerTag,
      "PartnerType": "Associates",
      "Marketplace": this.marketplace.name,
      "Operation": PaAPIOperation.GetItems.name
    };
    final response =
        await _post('/paapi5/getitems', body, PaAPIOperation.GetItems);

    print(response);
    return GetItemsResponse.fromJson(response);
  }

  Future<SearchItemsResponse> searchItems(
      String keywords, List<String> resources) async {
    final body = {
      "Keywords": keywords,
      "Resources": resources,
      "PartnerTag": this.partnerTag,
      "PartnerType": "Associates",
      "Marketplace": this.marketplace.name,
      "Operation": PaAPIOperation.SearchItems.name
    };
    final response =
        await _post('/paapi5/getitems', body, PaAPIOperation.SearchItems);
    return SearchItemsResponse.fromJson(response);
  }

  Future<dynamic> _post(
      String path, Map<String, dynamic> body, PaAPIOperation operation) async {
    var headers = _createHeaders(path, operation.name, body);
    var url = Uri.parse('https://$host$path');
    var response =
        await http.post(url, headers: headers, body: json.encode(body));
    var responseBody = utf8.decode(response.bodyBytes);
    return jsonDecode(responseBody);
  }

  Map<String, String> _createHeaders(
      String path, String operation, Map<String, dynamic> body) {
    final currentDateTime = new DateTime.now().toUtc();
    var headers = {
      'host': this.host,
      'content-type': 'application/json; charset=utf-8',
      'content-encoding': 'amz-1.0',
      'x-amz-date': _getTimeStamp(currentDateTime),
      'x-amz-target': 'com.amazon.paapi5.v1.ProductAdvertisingAPIv1.$operation',
    };

    final signedHeaders =
        (headers.keys.toList()..sort()).map((s) => s.toLowerCase()).join(';');

    String canonicalURL = _prepareCanonicalRequest(path, headers, body);
    String stringToSign = _prepareStringToSign(canonicalURL, currentDateTime);
    String signature = _calculateSignature(stringToSign, currentDateTime);

    headers['Authorization'] =
        _buildAuthorizationString(signedHeaders, signature, currentDateTime);

    return headers;
  }

  String _prepareCanonicalRequest(
      String path, Map<String, String> headers, Map<String, dynamic> payload) {
    final signedHeaders =
        (headers.keys.toList()..sort()).map((s) => s.toLowerCase()).join(';');

    final canonicalHeaders = headers.keys
        .map((key) => '${key.toLowerCase()}:${headers[key]?.trim()}')
        .toList()
      ..sort();

    var payloadHash =
        sha256.convert(utf8.encode(json.encode(payload))).toString();

    final canonicalURL = [
      'POST',
      path,
      '',
      ...canonicalHeaders,
      '',
      signedHeaders,
      payloadHash,
    ].join('\n');

    return canonicalURL;
  }

  String _prepareStringToSign(String canonicalURL, DateTime timestamp) {
    final credentialList = [
      _getDate(timestamp),
      this.region,
      this.service,
      aws4Request
    ];

    String stringToSign = [
      awsHmacAlgorithm,
      _getTimeStamp(timestamp),
      credentialList.join('/'),
      sha256.convert(utf8.encode(canonicalURL)).toString()
    ].join('\n');

    return stringToSign;
  }

  String _calculateSignature(String stringToSign, DateTime datetime) {
    var signatureKey = _getSignatureKey(datetime, this.region, this.service);
    var signature = Hmac(sha256, signatureKey)
        .convert(utf8.encode(stringToSign))
        .toString();
    return signature;
  }

  List<int> _getSignatureKey(DateTime datetime, String region, String service) {
    final credentialList = [
      _getDate(datetime),
      region,
      service,
      aws4Request,
    ];

    final signingKey = credentialList.fold(utf8.encode('AWS4$secretKey'),
        (List<int> key, String s) {
      final hmac = Hmac(sha256, key);
      return hmac.convert(utf8.encode(s)).bytes;
    });

    return signingKey;
  }

  String _buildAuthorizationString(
      String signedHeaders, String signature, DateTime datetime) {
    return '$awsHmacAlgorithm Credential=$accessKey/${_getDate(datetime)}/$region/$service/$aws4Request, ' +
        'SignedHeaders=$signedHeaders, ' +
        'Signature=$signature';
  }

  String _getTimeStamp(DateTime datetime) {
    return new DateFormat("yyyyMMdd'T'HHmmss'Z'").format(datetime);
  }

  String _getDate(DateTime datetime) {
    return new DateFormat("yyyyMMdd").format(datetime);
  }
}

And I use it like this:

  static Future<void> lookUpBarcode(String value) async {
    // list all resources you want to fetch
    var resources = [
      PaAPIResource.Images_Primary_Small.name,
      PaAPIResource.ItemInfo_Title.name,
      PaAPIResource.Offers_Listings_Price.name,
      PaAPIResource.Offers_Listings_SavingBasis.name,
    ];

    PaAPI paAPI = PaAPI(
      accessKey: 'my-access-key',
      secretKey: 'my-secret-key',
      partnerTag: AffiliatePartners.amazonCom.id,
      // marketplace is defaulting to US. Use the PaAPIMarketplace enum to select your marketplace
      marketplace: PaAPIMarketplace.us,
    );

    final product = await paAPI.getItems([value], resources);

    print(product);
  }

I simply can not figure out how to correclty sign the request. Has anyone successfully got it working with Flutter? Happy for every help!

Let me know if you need more information.

0

There are 0 best solutions below