EDUCAÇÃO E TECNOLOGIA

Passing Dynamic Signature (HMAC SHA256 algorithm) and Timestamp in SAP PI REST Adapter Header Using Java Mapping

Introduction:

This Blog will guide you to pass dynamic Signature and Timestamp in the Header of a POST Method Using REST Adapter in SAP PI. For calculating signature there are certain rules which will be mentioned in the Pre-Request Script of the Third-party service which you will be integrating into SAP. In this case, the Signature is calculated by the HMAC SHA256 algorithm and passed in the Header of the POST Service where the receiving third-party system will validate the signature.

Preface:

I recently worked with a Receiver Adapter integration with a Third-party and there was a challenge in Passing Headers. In general Integration scenarios in Headers, we used to pass Token or Authorization or some known parameters, the case which I have worked on has Signature and Timestamp to be passed in the Headers.

The calculation of Signature has some challenges where we have to use certain algorithms with some parameters. The Signature which we generate and send in the Headers will be validated by the third party and access will be provided.

It was difficult for me to implement this type of scenario and it was very hard to find blogs in such cases. So, I thought of sharing my experience through this blog.

Requirement:

Passing dynamic Signature and Timestamp in the Header of a POST Method Using REST Adapter in SAP PI

Sample POST service:

Postman%20Sample

Postman Sample

In Headers of a post-service Time Stamp and Signature which has Host, Method, Request Type, and Secret Key must be kept

The Signature is calculated by HMAC SHA256 algorithm. For this algorithm to calculate the signature Request Method, Host, Method Type, Time Stamp and the Json Request Content is needed.

Example:

POST URL General Format: “https” /  “Host”  / “Method”

Request Content:

{

“Id”: “123”,

“Type”: “Sample Type”

}

Signature Calculation parameters:

Host: Host Name

Method: Method Name

Request Method: POST

Secret Key: client Id and client secret Id encoded

Note: In the sample postman collection, which you will get for the requirement there will be a pre-request script that will give details about the signature calculation methods.

Configuration Steps:

Create Data Type, Message Type, Service Interface, Message Mapping, and Operation Mapping of Request and Response in ESR.

Create and Import Java Mapping Archive using the below Java Code

package sap.com; import com.sap.aii.mapping.api.*;
import org.json.*;
import java.io.*;
import java.util.ArrayList;
import java.util.List;
import java.util.Date; import org.apache.commons.codec.binary.Base64;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec; public class CalculateSignaturePost extends AbstractTransformation { private List<String> array_nodes = new ArrayList<String>(); // Adding Dynamic Attributes private static final DynamicConfigurationKey Sign = DynamicConfigurationKey .create("http://sap.com/xi/XI/System/REST", "Signature"); private static final DynamicConfigurationKey TimeStamp = DynamicConfigurationKey .create("http://sap.com/xi/XI/System/REST", "TimeStamp"); @Override public void transform(TransformationInput transformationInput, TransformationOutput transformationOutput) throws StreamTransformationException { try { // Getting Input Parameters - Secret Key, Host, Request Method, Method String SecretKey = transformationInput.getInputParameters().getString("Secret_Key"); String Host = transformationInput.getInputParameters().getString("Host"); String Method = transformationInput.getInputParameters().getString("Method"); String RequestMethod = transformationInput.getInputParameters().getString("Request_Method"); InputStream inputstream = transformationInput.getInputPayload().getInputStream(); String sourcexml = ""; String targetxml = ""; String line = ""; BufferedReader br = new BufferedReader(new InputStreamReader(inputstream)); while ((line = br.readLine()) != null) sourcexml += line + "\n"; br.close(); // Converts XML to JSON JSONObject xmlJSONObj = XML.toJSONObject(sourcexml); // Making the Node as Array and Converting Numbers to string array_nodes.add("arraynode"); xmlJSONObj = handleJSONData(xmlJSONObj); // Converts JSON to string String jsonstring = xmlJSONObj.toString(0); //Removing Namespace Tag jsonstring = jsonstring .replaceAll("\"" + "xmlns:ns0" + "\":" + "\"" + "your namespace" + "\"" + ",", ""); //Removing the outer element jsonstring = jsonstring.replaceAll("\"" + "ns0:Message Type Name" + "\":", ""); int len = jsonstring.length(); len = len - 1; jsonstring = jsonstring.substring(1, len); // Encoding JSON Message string to base64 jsonstring = jsonstring.trim(); Base64 base64 = new Base64(); String jsonBase64 = new String(base64.encode(jsonstring.getBytes())); // Getting Current TimeStamp Long CurrentTimeStamp = new Date().getTime(); getTrace().addDebugMessage("TimeStamp = " + CurrentTimeStamp); // Creating the string to calculate signature String stringToSing = RequestMethod + "\n" + Host+ "\n" +Method+ "\n" + "id=" + "&t="+CurrentTimeStamp+ "&ed=" + jsonBase64; getTrace().addDebugMessage(stringToSing); // Calculating signature using HMAC SHA256 algorithm Mac sha256_HMAC = Mac.getInstance("HmacSHA256"); SecretKeySpec secret_key = new SecretKeySpec(SecretKey.getBytes(), "HmacSHA256"); sha256_HMAC.init(secret_key); // Encoding signature calculated to base64 String hash = Base64.encodeBase64String(sha256_HMAC.doFinal(stringToSing.getBytes())); getTrace().addDebugMessage("Signature = " + hash); // Add Signature & Timestamp as dynamic attributes DynamicConfiguration conf = transformationInput.getDynamicConfiguration(); conf.put(Sign, hash); conf.put(TimeStamp, String.valueOf(CurrentTimeStamp)); targetxml = jsonstring.trim(); transformationOutput.getOutputPayload().getOutputStream().write(targetxml.getBytes("UTF-8")); } catch (Exception exception) { getTrace().addDebugMessage(exception.getMessage()); throw new StreamTransformationException(exception.toString()); } } public JSONObject handleJSONData(JSONObject jsonObj) { /* * Parse the JSON Structure to Delete a record or convert it to an array * Input: JSONObject -> Json Sub structure to be updated Output: * JSONObject -> Updated Json Sub structure with deleted records and * arrays. */ try { // Create an array of keyset to loop further String arr[] = new String[jsonObj.keySet().size()]; int k = 0; for (String key : jsonObj.keySet()) arr[k++] = key; // Loop through all the keys in a JSONObject for (String key : arr) { // If there are records to be converted to Array, convert it. if (array_nodes.contains(key)) { jsonObj = forceToJSONArray(jsonObj, key); } // If the sub node is a JSONArray or JSONObject, step inside the // Object if (jsonObj.get(key) instanceof JSONArray) { JSONArray sjao = jsonObj.getJSONArray(key); for (int i = 0; i < sjao.length(); i++) { sjao.put(i, handleJSONData(sjao.getJSONObject(i))); } jsonObj.put(key, sjao); } else if (jsonObj.get(key) instanceof JSONObject) { jsonObj.put(key, handleJSONData(jsonObj.getJSONObject(key))); } else { // Convert number to String Object val = jsonObj.get(key); if (val instanceof Integer || val instanceof Float || val instanceof Double || val instanceof Long || val instanceof Short) jsonObj.put(key, jsonObj.get(key).toString()); } } } catch (Exception e) { // Handle all exceptions if (getTrace() != null) { getTrace().addDebugMessage("Exception while Updating Payload: ", e); } else e.printStackTrace(); } return jsonObj; } public static JSONObject forceToJSONArray(JSONObject jsonObj, String key) throws org.json.JSONException { /* * Force Convert a record to JSON Array Input: 1) JSONObject -> JSON Sub * structure to be updated 2) key -> Key whose value is to be converted * to JSONArray Output: JSONObject -> Updated Json Sub structure with * deleted records and arrays. */ // Get the key value from JSONObject using opt() and not get(), as it // can also return null value. Object obj = jsonObj.opt(key); // If the obj doesn't exist inside my the JsonObject structure, create // it empty if (obj == null) { jsonObj.put(key, new JSONArray()); } // if exist but is a JSONObject, force it to JSONArray else if (obj instanceof JSONObject) { JSONArray jsonArray = new JSONArray(); jsonArray.put((JSONObject) obj); jsonObj.put(key, jsonArray); } // if exist but is a primitive entry, force it to a "primitive" // JSONArray else if (obj instanceof String || obj instanceof Integer || obj instanceof Float || obj instanceof Double || obj instanceof Long || obj instanceof Boolean) { JSONArray jsonArray = new JSONArray(); jsonArray.put(obj); jsonObj.put(key, jsonArray); } return jsonObj; } }

Add the Java Mapping Archive to Operation Mapping of the Request Content.

Java%20Class%20in%20Operation%20Mapping%20and%20Binding

Java Class in Operation Mapping and Binding

In the Highlighted Binding of Java Class create the Input parameters which must be passed to the Java class.

Binding%20Parameters

Binding Parameters

In Integration Configuration when you import the Operation Mapping in Receiver Interface you will get the option to pass the Parameters (Host, Method, Request Method, Secret Key)

Setting%20Parameters%20in%20Integration%20Configuration

Setting Parameters in Integration Configuration

The response coming from the Java Class will have two Parameters (TimeStamp and Signature). Those two parameters have to be passed in the HTTP Headers of the communication channel.

Passing%20Dynamic%20Headers%20in%20Communication%20Channel

Passing Dynamic Headers in Communication Channel

The signature from the Java Mapping will be mapped to the HTTP headers and will be passed to the external service.

This brings me to the conclusion of this blog. Please feel free to comment if you require any clarifications regarding the code.

Thanks & Regards

Thamarai Selvan

SAP Technical Consultant