Skip to Content

Introduction:

It’s a pleasure to share our project experience on integrating C4C with AWS DynamoDB via CPI.  Amazon DynamoDB is a nonrelational database, you can create database tables that can store and retrieve any amount of data. However it does not have an endpoint url. The key aspect of this integration is the authentication mechanism which is a bit different from other integrations. AWS expects the authentication signature in a specific format, namely combination of authorization & header parameters.

With this blog, we wanted to share the steps that are needed to push data from C4C to DynamoDB table.

 

Main Part:

E2E Process Flow:

 

 

Step1:

Push data from C4C to CPI. This is a pdi development wherein on save of an Activity, we are sending Activity related information to CPI in json format. In CPI, we are converting this payload into XML via standard JSON-XML convertor.

Sender will be http adapter which will trigger the outbound process from C4C:

Step2:

In the message mapping, we will map C4C xml with the table fields of DynamoDB table

Step3:

Convert the Message Mapping output from XML to JSON via JSON convertor because DynamoDB can accept incoming request in json format.

Step4:

Set Message headers: AWS expects the incoming payload to have the headers in a specified format. Message header should have the below parameters:

-> Content-Type = application/x-amz-json-1.0
-> X-Amz-Target = DynamoDB_20120810.PutItem
-> Host = location of AWS server (dynamodb.ap.XXXXX.com)

Below is the snapshot of Postman which will help in building the AWS Signature:

 

Step5:

Generate AWS Signature:

Signature Version 4 is the process to add authentication information to AWS requests sent by HTTP. Requests to AWS must be signed with an access key, which consists of an access key ID and secret access key. These two keys are commonly referred to as your security credentials. We referred to the below links to create the signature. The generated signature should match with the one that’s dynamically created by AWS at runtime.

https://docs.aws.amazon.com/general/latest/gr/signature-version-4.html

https://docs.aws.amazon.com/AmazonS3/latest/API/sig-v4-authenticating-requests.html

https://docs.aws.amazon.com/general/latest/gr/sigv4_signing.html

Store all the secured header information in the Security Materials in CPI:

Groovy script to generate the signature:

import com.sap.gateway.ip.core.customdev.util.Message;
import java.util.HashMap;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.security.MessageDigest;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import com.sap.it.api.ITApiFactory;
import com.sap.it.api.securestore.SecureStoreService;
import com.sap.it.api.securestore.UserCredential;

def Message processData(Message message) {
    //************* REQUEST VALUES *************
    String method = 'POST';
     def headers = message.getHeaders();
     String host = headers.get("Host");
     String endpoint = 'https://'+ host+'/'
    // POST requests use a content type header. For DynamoDB, the content is JSON.
    String amz_target = headers.get("X-Amz-Target");
    String content_type = headers.get("Content-Type");
    
    // Request parameters for Create/Update new item--passed in a JSON block.
    String body = message.getBody(java.lang.String) as String;
    String request_parameters = body;
    
   // Read AWS access key from security artifacts. 
   def secureStoreService = ITApiFactory.getApi(SecureStoreService.class, null);
   //AWS_ACCESS_KEY
   def aws_access_key = secureStoreService.getUserCredential("AWS_ACCESS_KEY");
   if (aws_access_key == null){
      throw new IllegalStateException("No credential found for alias 'AWS_ACCESS_KEY' ");
   }
   //AWS_SECRET_KEY
  def aws_secret_key = secureStoreService.getUserCredential("AWS_SECRET_KEY");
   if (aws_secret_key == null){
      throw new IllegalStateException("No credential found for alias 'AWS_SECRET_KEY' ");
   }
   //AWS_REGION
   def aws_region = secureStoreService.getUserCredential("AWS_REGION");
   if (aws_region == null){
      throw new IllegalStateException("No credential found for alias 'AWS_REGION' ");
   }
   
   //AWS_SERVICE_NAME
   def aws_service = secureStoreService.getUserCredential("AWS_SERVICE_NAME");
   if (aws_service == null){
      throw new IllegalStateException("No credential found for alias 'AWS_SERVICE_NAME' ");
   }
   
    String access_key = new String(aws_access_key.getPassword());
    String secret_key = new String(aws_secret_key.getPassword());
    String service =   new String(aws_service.getPassword());
    String region =   new String(aws_region.getPassword());
   
    // Create a date for headers and the credential string
    def date = new Date();
    DateFormat dateFormat = new SimpleDateFormat("yyyyMMdd'T'HHmmss'Z'");
    dateFormat.setTimeZone(TimeZone.getTimeZone("UTC"));//server timezone
    String amz_date = dateFormat.format(date);
    dateFormat = new SimpleDateFormat("yyyyMMdd");
    dateFormat.setTimeZone(TimeZone.getTimeZone("UTC"));//server timezone
    String date_stamp = dateFormat.format(date);
    
    // ************* TASK 1: CREATE A CANONICAL REQUEST *************
    // http://docs.aws.amazon.com/general/latest/gr/sigv4-create-canonical-request.html
    
    // Step 1 is to define the verb (GET, POST, etc.)--already done.
    
    // Step 2: Create canonical URI--the part of the URI from domain to query 
    // string (use '/' if no path)
    String canonical_uri = '/';
    
    // Step 3: Create the canonical query string. In this example, request
    // parameters are passed in the body of the request and the query string is blank.
    String canonical_querystring = '';
    
    // Step 4: Create the canonical headers. Header names must be trimmed
    // and lowercase, and sorted in code point order from low to high. Note that there is a trailing \n.
    String canonical_headers = 'content-type:' + content_type + '\n' + 'host:' + host + '\n' + 'x-amz-date:' + amz_date + '\n' + 'x-amz-target:' + amz_target + '\n';
    
    // Step 5: Create the list of signed headers. This lists the headers
    // in the canonical_headers list, delimited with ";" and in alpha order.
    // Note: The request can include any headers; canonical_headers and
    // signed_headers include those that you want to be included in the
    // hash of the request. "Host" and "x-amz-date" are always required.
    // For DynamoDB, content-type and x-amz-target are also required.
    String signed_headers = 'content-type;host;x-amz-date;x-amz-target';
    
    // Step 6: Create payload hash. In this example, the payload (body of the request) contains the request parameters.
    String payload_hash = generateHex(request_parameters);
    
    // Step 7: Combine elements to create canonical request
    String canonical_request = method + '\n' + canonical_uri + '\n' + canonical_querystring + '\n' + canonical_headers + '\n' + signed_headers + '\n' + payload_hash;
    
    // ************* TASK 2: CREATE THE STRING TO SIGN*************
    // Match the algorithm to the hashing algorithm you use, either SHA-1 or SHA-256 (recommended)
    String algorithm = 'AWS4-HMAC-SHA256';
    String credential_scope = date_stamp + '/' + region + '/' + service + '/' + 'aws4_request';
    String string_to_sign = algorithm + '\n' +  amz_date + '\n' +  credential_scope + '\n' +  generateHex(canonical_request);
    
    // ************* TASK 3: CALCULATE THE SIGNATURE *************
    // Create the signing key using the function defined above.
    byte[] signing_key = getSignatureKey(secret_key, date_stamp, region, service);
    
    // Sign the string_to_sign using the signing_key
    byte[] signature = HmacSHA256(string_to_sign,signing_key);
    
     /* Step 3.2.1 Encode signature (byte[]) to Hex */
    String strHexSignature = bytesToHex(signature);
    
    // ************* TASK 4: ADD SIGNING INFORMATION TO THE REQUEST *************
    // Put the signature information in a header named Authorization.
    String authorization_header = algorithm + ' ' + 'Credential=' + access_key + '/' + credential_scope + ', ' +  'SignedHeaders=' + signed_headers + ', ' + 'Signature=' + strHexSignature;
    
    // For DynamoDB, the request can include any headers, but MUST include "host", "x-amz-date",
    // "x-amz-target", "content-type", and "Authorization". Except for the authorization
    // header, the headers must be included in the canonical_headers and signed_headers values, as
    // noted earlier. Order here is not significant.

    // set X-Amz-Date Header and Authorization Header
    message.setHeader("X-Amz-Date",amz_date);
    message.setHeader("Authorization", authorization_header);
      
    return message;
}

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();
} 

String generateHex(String data) {
    MessageDigest messageDigest;

    messageDigest = MessageDigest.getInstance("SHA-256");
    messageDigest.update(data.getBytes("UTF-8"));
    byte[] digest = messageDigest.digest();
    return String.format("%064x", new java.math.BigInteger(1, digest));
}

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"));
}

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;
}

 

Step6:

Post the json payload to DynamoDB through a Request/Reply step using http adapter

 

Step7:

The response from DynamoDB sent back to C4C in the form of status code. Ex:Code 200 means success, 400 means Bad Request.

Step8:
Exceptions are handled via Exception Subprocess

 

Sample payload to create an entry in DynamoDB table:

 

You can refer to the below link for code snippets on CRUD operations for DynamoDB table:

https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_PutItem.html


Conclusion:

We learnt what it takes to set up the integration between C4C and DynamoDB via CPI.

To report this post you need to login first.

1 Comment

You must be Logged on to comment or reply to a post.

Leave a Reply