Technology Blogs by Members
Explore a vibrant mix of technical expertise, industry insights, and tech buzz in member blogs covering SAP products, technology, and events. Get in the mix!
cancel
Showing results for 
Search instead for 
Did you mean: 
rhviana
Active Contributor
2,811
Hello Folks,

One more interesting blog and sharing the knowledge and experience with you, basically let’s follow the topics and instructions during the reading.




Agenda:








  1. Introduction




  2. Scenario and Integration Perspective




  3. Amazon API services




  4. Adapters available in SAP CPI



    1. Open Connector Amazon

    2. HTTPS

    3. Amazon Web Services Adapter for SAP Integration Suite




  5. AWS Signature via groovy – Method POST



    1. Problem to use MessageDigest – SHA256 – in groovy

    2. Method GET




  6. Integration Flow in SAP CPI using HTTPS Adapter




  7. Checking the result of the print label document



  8. Result






Introduction:




In below blog I would like to share how we can integrate Amazon API services Bucket service using the HTTPS standard adapter and doing the AWS Header Signature via groovy.

You are going to see some similarities with previous blog of signature but you don't need use the code for the list of services below because in this case there is a free adapter in place for that.

Amazon Web Services Adapter for SAP Integration Suite:

Feature highlights:

  • Supports for S3, and SQS protocols on the sender side.

  • Supports for S3, SQS, SNS, and SWF protocols on the receiver side.

  • AWS S3 to read and push files from and into AWS S3 service.

  • S3: Append timestamp to the file name.

  • S3: Option to choose storage class.

  • S3: Existing file handling option.

  • S3: Server-Side Encryption.

  • S3: Add customer metadata.

  • S3: Option to list objects.

  • AWS SQS to read and send a message from and to an AWS SQS queue.

  • SQS: For standard queue, the option to provide a delay.

  • SQS: For the FIFO queue, the option to provide message deduplication id and message group ID.

  • SQS: Large message handling.

  • AWS SNS to push real-time notification messages to interested subscribers over multiple delivery protocols.

  • SNS: Option to provide Identical/Custom Payload.

  • SNS: Option to provide Message attribute.

  • SNS: Support for FIFO SNS Topic.

  • SNS: Large message handling.

  • AWS SWF to provide full control over implementing tasks and coordinating them.

  • SWF: Option to determine request and response format.


 




Scenario and Integration perspective:




The scenario is the integration of SAP S4 with Amazon Label Print document Creation API


The SAP CPI will be responsible to receive the call, with parameters as client_id, client_secret and refreshed_token * and forward to AWS in the seguency above to in the end receive a response with BASE64 of the label to be print and ship to the receive customer.

The free adapter of AMAZON available in SAP CPI can not be used for this service call, in duty of that basically once again I adjust the previous groovy script code that I provided in the past blog with some changes where I will explain you in details below.

Previous blog: sap-cpi-amazon-s3-integration-with-https-adapter

Using the HTTPS adapter with the POST and GET to receive the label Base64 file, the conversion for Base64 to image will be done in the backend system but I will present you the result using some on-line pages - From BASE64 to Image.

*** Important information the code must be change for the correct details and others about your AWS Server ***




Amazon API Services






An Amazon API Services available to execute some process, I'm not going in deep details here you can take a look in the links below to understand more about.

Token: Tokens API Use Case Guide (amazon.com)

Create Shipment Labels: Vendor Direct Fulfillment Shipping API v2021-12-28 reference (amazon.com)




Adapters available in SAP CPI







  • Open Connector Amazon


For this adapter there is a fantastic blog from sriprasad.shivarambhat explaining clearly how to setup and make the configuration in case that your company has the license for OpenConnector for SAP CPI

Link of the Blog: Amazon Open Connector

  • HTTPS


This is the adapter that I choose for this scenario, basic and traditional HTTP(S) call with methods and authentication, in this case the authentication is made by groovy you will see later the configuration.

  • Amazon Web Services Adapter


More details about this adapter – SAP API Hub

This adapter can not be used for this type of integration because there is no API header signature.




AWS Signature via groovy – Method POST






I will not explain in the details whole process of the signature, you can easily check in the Amazon Header Signature V4.

To generate the signature header v4 basically it is compose of 3 steps with the details of Amazon:







  • 1 Step: Canonical Request












  • 2 Step: String-to-Sing













  • 3 Step :Deriver- signing-key










byte [ ] signing_key =
byte[] getSignatureKey(String key, String dateStamp, String regionName, String serviceName) throws Exception {
byte[] kSecret = ("AWS4" + key).getBytes("UTF8");
byte[] kDate = HmacSHA256(dateStamp, kSecret);
byte[] kRegion = HmacSHA256(regionName, kDate);
byte[] kService = HmacSHA256(serviceName, kRegion);
byte[] kSigning = HmacSHA256("aws4_request", kService);
return kSigning;
}





The details of each step is clear ?






I generated one groovy script calling different functions across the Iflow as you can see:

  • getTokens

  • getRestrictedDataToken

  • signatureAWS

  • getContentB64


I hope so, now you will see with more details in the groovy script:

 

Important topic the small differences between this code and previous released code is:




Signature S3 Bucket:




String method = "PUT";    
String host = "<yourbuket>.s3.us-east-2.amazonaws.com";
String region = "us-east-2";
String service = "s3";
String endpoint = "s3.us-east-2.amazonaws.com";

String canonical_uri = "/<yourFolder>/"+map.get('NomeArquivo');

String canonical_headers = "host:" + host + "\n"+ "x-amz-content-sha256:" + hashBody + "\n" + "x-amz-date:" + amzDate + "\n";





Signature API:




String method = "POST";
String region = "us-west-2";
String service = "execute-api";
String host = "us-east-2.amazonaws.com";

String canonical_uri = "https://sellingpartnerapi-fe.amazon.com/tokens/2021-03-01/restrictedDataToken";

String canonical_headers = "accept: application/json"+";\n"+"content-length:"+ body.length() + ";\n"+"content-type:application/json"+";\n"+"host:" + host + ";\n"+ "x-amz-content-sha256:" + hashBody + ";\n" + "x-amz-date:" + amzDate + ";\n";

 




Full groovy:




/*****************************************************************
*
* Developer: Viana - SAP Senior Integration Consultant
*
******************************************************************/
/************
* *
* Libraries *
* *
*************/

import com.sap.gateway.ip.core.customdev.util.Message;
import java.util.HashMap;
import groovy.json.*;
import com.sap.gateway.ip.core.customdev.util.Message;
import javax.crypto.Mac
import javax.crypto.spec.SecretKeySpec
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.text.SimpleDateFormat
import java.lang.Object
import java.util.List;
import java.util.TimeZone;
import org.apache.commons.codec.digest.DigestUtils;


/*******************************************************************************************************************************************************************************************
* *
* STEP - 1 : Description of function getTokens: Groovy to extract the values from the attributes "access_token" and "refresh_token" generate properties that will be use in the next calls *
* *
********************************************************************************************************************************************************************************************/

def Message getTokens(Message message) {
Reader reader = message.getBody(Reader)
def json = new JsonSlurper().parse(reader)
message.setProperty("TokenAWS", json.access_token)
message.setProperty("RefreshTokenAWS",json.refresh_token)
return message
}

/***************************************************************************************************************************************************************************************************************
* *
* STEP - 2 - Description of functiongetRestrictedDataToken: Groovy to extract the value from the attribute restrictedDataToken and create the property "restrictedDataToken" that will be use in the next call *
* *
****************************************************************************************************************************************************************************************************************/

def Message getRestrictedDataToken (Message message){
Reader reader = message.getBody(Reader)
def json = new JsonSlurper().parse(reader)
message.setHeader("x-amz-access-token", json.restrictedDataToken)

return message
}

/************************************************************************************************
* *
* STEP - 3 - Description of function signatureAWS : Groovy to generate the AWS signature header *
* *
*************************************************************************************************/

def Message signatureAWS(Message message) {
def body = message.getBody(java.lang.String) as String
//************* Hash do Body using apache commons DigestUtils sha256Hex *************
def hashBody = DigestUtils.sha256Hex(body)
//************* Mapping the properties - The filename was set in previous groovyScript *************
def map = message.getProperties()
//************* Iniciating variables *************
String method = "POST";
String region = "us-west-2";
String service = "execute-api";
String host = "us-east-2.amazonaws.com";
// Read AWS access key from security artifacts. Best practice is NOT to embed credentials in code.
def access_key = <your details as property from content modifier>
def secret_key = <your details as property from content modifier>
// Create a date for headers and the credential string
def now = new Date()
def amzFormat = new SimpleDateFormat( "yyyyMMdd'T'HHmmss'Z'" )
def formattedDate = new SimpleDateFormat("EEEE, MMMM dd, yyyy, hh:mm a '('zzz')'")
def stampFormat = new SimpleDateFormat( "yyyyMMdd" )
def amzDate = amzFormat.format(now)
def date_stamp = stampFormat.format(now)
//************* Canonical Request variables *************
String canonical_uri = "https://sellingpartnerapi-fe.amazon.com/tokens/2021-03-01/restrictedDataToken";
String canonical_querystring = "";
String canonical_headers = "accept: application/json"+";\n"+"content-length:"+ body.length() + ";\n"+"content-type:application/json"+";\n"+"host:" + host + ";\n"+ "x-amz-content-sha256:" + hashBody + ";\n" + "x-amz-date:" + amzDate + ";\n";
message.setProperty("Canonical_Headers",canonical_headers)
String signed_headers = "accept;content-length;content-type;host;x-amz-content-sha256;x-amz-date";
String canonical_request = method + "\n" + canonical_uri + "\n" + canonical_querystring + "\n" + canonical_headers + "\n" + signed_headers + "\n" + hashBody;
//************* Sing to Sing variables *************
String algorithm = "AWS4-HMAC-SHA256";
String credential_scope = date_stamp + "/" + region + "/" + service + "/" + "aws4_request";
String string_to_sign = algorithm + "\n" + amzDate + "\n" + credential_scope + "\n" + DigestUtils.sha256Hex(canonical_request);
//************* Generating the Singning Key *************
byte[] signing_key = getSignatureKey(secret_key, date_stamp, region, service);
//************* Generating the HmacSHA256 - Amazon *************
byte[] signature = HmacSHA256(string_to_sign,signing_key);
//************* Generating the Hex of the Signature *************
String strHexSignature = bytesToHex(signature);
//************* Generating the authorization header signed - Amazon V4 S3 Bucket *************
String authorization_header = algorithm + " " + "Credential=" + access_key + "/" + credential_scope + ", " + "SignedHeaders=" + signed_headers + ", " + "Signature=" + strHexSignature;
//************* Seting the headers of HTTP call *************
message.setHeader("x-amz-date",amzDate);
message.setHeader("x-amz-content-sha256", hashBody)
message.setHeader("Authorization", authorization_header);
message.setHeader("Host", "us-east-2.amazonaws.com");
message.setHeader("Content-type", "application/json");
message.setHeader("x-amz-access-token", message.getProperty("TokenAWS"))
message.setHeader("Accept","application/json")
//************* Setting the body to be store in Amazon *************
message.setBody(body)
return message
}

/**********************************************
* *
* Function to convert bytes to Hexadecimal *
* *
***********************************************/
String bytesToHex(byte[] bytes) {
char[] hexArray = "0123456789ABCDEF".toCharArray();
char[] hexChars = new char[bytes.length * 2];
for (int j = 0; j < bytes.length; j++) {
int v = bytes[j] & 0xFF;
hexChars[j * 2] = hexArray[v >>> 4];
hexChars[j * 2 + 1] = hexArray[v & 0x0F];
}
return new String(hexChars).toLowerCase();
}

/********************************************
* *
* Function to HmacSHA256 *
* *
*********************************************/

byte[] HmacSHA256(String data, byte[] key) throws Exception {
String algorithm="HmacSHA256";
Mac mac = Mac.getInstance(algorithm);
mac.init(new SecretKeySpec(key, algorithm));
return mac.doFinal(data.getBytes("UTF8"));
}
/**********************************************
* *
* Function to Get signature *
* *
***********************************************/

byte[] getSignatureKey(String key, String dateStamp, String regionName, String serviceName) throws Exception {
byte[] kSecret = ("AWS4" + key).getBytes("UTF8");
byte[] kDate = HmacSHA256(dateStamp, kSecret);
byte[] kRegion = HmacSHA256(regionName, kDate);
byte[] kService = HmacSHA256(serviceName, kRegion);
byte[] kSigning = HmacSHA256("aws4_request", kService);
return kSigning;
}

/*****************************************************************************************************************
* *
* STEP - 4 - Extract the value from the attribute "content" in base64 and return the string as a final response *
* *
******************************************************************************************************************/

def Message getContentB64 (Message message){
Reader reader = message.getBody(Reader)
def json = new JsonSlurper().parse(reader)
String base64AwsResponse = json.labelData.content
message.setBody(base64AwsResponse.substring( 1, base64AwsResponse.length() - 1 ) )
return message
}






Problems to use the Message Digest – SHA256 – in Groovy.

Lib: import java.security.MessageDigest;

Function:
def generateHex(String data) {
MessageDigest mac = MessageDigest.getInstance("SHA-256");
byte[] signatureBytes = mac.digest(data.getBytes(StandardCharsets.UTF_8));
StringBuffer hexString = new StringBuffer();
for (int j=0; j<signatureBytes.length; j++) {
String hex=Integer.toHexString(0xff & signatureBytes[j]);
if(hex.length()==1) hexString.append('0');
hexString.append(hex);
}
String encryptedSignature = hexString.toString();
String encryptHash = encryptedSignature.replace("-","");
encryptHash = encryptHash.toLowerCase();
return encryptHash;
}

Error about Security Exception in the line 83 in yellow:






To solve the problem is soft, basically import the lib import org.apache.commons.codec.digest.DigestUtils; in your code.

Lib: import org.apache.commons.codec.digest.DigestUtils;

Replace the function to generate SHA-256 HEX using the function sha256Hex from DigestUtils

def hashBody = DigestUtils.sha256Hex(body)


Link for more details: sha256Hex-java.lang.String

Different methods to develop SHA-256:

Generate SHA-256




 

 

Integration Flow in SAP CPI – HTTPS Adapter







This flow is sample of POC, I can't provide full details of real Iflow but you can get a taste how to adapt it for your project needs.

Also you can see the exception handle empty, also you need adapt for your project my suggestion in case of any failure during the steps, backend should resend the message.

Basically the SAP S4 is sending the data via SOAP and let's go over the 10 steps of the flow.






  1. Create the content-type: application/x-www-form-urlencoded





    1. I decide use a content modifier generating the headers and the body as you can see below:











    2. Body - content-type: application/x-www-form-urlencoded




      grant_type=${header.grant_type}&refresh_token=${header.refresh_token}&client_id=${header.client_id}&client_secret=${header.client_secret}












  2. Call the Tokens API's





    1. From the response below you should extract the refresh_token attribute value
      I create the property with the value because you should use in the next call in the header.








  3. Groovy function: getTokens





  4. Content Modifier - Hard code body value as per of the requirements from Amazon, in this case to create a shipping label the body should be.


    1. {
      "restrictedResources": [
      {
      "method": "POST",
      "path": "/vendor/directFulfillment/shipping/2021-12-28/shippingLabels/{purchaseOrderNumber}"
      }
      ]
      }









  5. Groovy function: signatureAWS





  6.  Call the next API for Data Token - Response:









  7. Groovy function: getRestrictedDataToken with the result of second call you should set the header parameter x-amz-access-token with the value.

    1. This header x-amz-access-token is used also in the second call but with the value result from the first call








  8. Content modifier with a dummy body I extract from the Amazon site

    1. {
      "sellingParty": {
      "partyId": "H174I"
      },
      "shipFromParty": {
      "partyId": "EQQB"
      }
      }








  9. Call the next API to receive BASE64 of the label as string - response:









  10. Groovy function: getContentB64 - extract the content and response to S4/hana responsable to do the converstion B64 to PNG, but also you can use the function from CPI as you wish its going to produce the same result






Checking the result of the print label document




To check the image, first you need decode the bas64 using: Base64 Decode and Encode - Online


Than copy the result of the decoding, as you can see using also decoding with CPI generates the same result











To see the real label from the result of decoding BASE64 check this site: Labelary Online ZPL Viewer

 

Important to mention that also you can decide to return direct the image to SAP using the public API from ZPL Viewer:

 

API Service link

 

Sample of call via Postman or Browser than you can decide the content-type you would like:

  • PNG (requested by sending an Accept: image/png request header, or by omitting the Accept request header entirely)

  • PDF (requested by sending an Accept: application/pdf request header)

  • IPL (requested by sending an Accept: application/ipl request header)

  • EPL (requested by sending an Accept: application/epl request header)

  • DPL (requested by sending an Accept: application/dpl request header)

  • JSON (requested by sending an Accept: application/json request header, useful for data extraction)


Execute the link bellow in your browser:

Link for test




 

Postman result with header with Accept: application/pdf :







Results:




Success !!!







I hope that you enjoy the read that gives you a overview and possibilities to use the API's from Amazon with SAP Integration Suite to support your business.

Kind regards,

Viana.
2 Comments
Labels in this area