Skip to Content
Technical Articles

Consuming GraphQL API hosted on AppSync-AWS from SAP CPI

Introduction:

In our last Blog, We have seen how to Integrate SAP CPI with AWS DynamoDB service. Now, We are going to focus on the GraphQL API hosted on AppSync in AWS.

I have got a requirement wherein I need to integrate with Web Application hosted on AWS Cloud. To consume the data from the server, a GraphQL API has been created using AppSync service in AWS and the same needs to be consumed by SAP CPI as a part of integration with the help of GraphQL Schema.

In this Blog, we are going to see on how to consume GraphQL API hosted on AWS AppSync from Postman. Post then, we will see on how to consume the same from SAP Cloud Platform Integration (CPI).

Prerequisite:

  •  Knowledge on SAP CPI
  •  Knowledge on AWS
  •  Knowledge on GraphQL Query and GraphQL Schema.

Overview of GraphQL API:

  • GraphQL is a specification for how to talk to an API. It’s typically used over HTTP where the key idea is to POST a “query” to an HTTP endpoint, instead of hitting different HTTP endpoints for different resources.
  • GraphQL is designed for developers of web/mobile apps (HTTP clients) to be able to make API calls to fetch exactly the data they need from their backend APIs.
  • Note: The GraphQL query is not really JSON. It looks like the shape of the JSON you want. So, when we make a ‘POST’ request to send our GraphQL query to the server, it is sent as a “string” by the client.
  • The server gets the JSON object and extracts the query string. As per the GraphQL syntax and the graph data model (GraphQL schema), the server processes and validates the GraphQL query.
  • Just like a typical API server, the GraphQL API server then makes calls to a database or other services to fetch the data that the client requested. The server then takes the data and returns it to the client in a JSON object. To know more, Please check GraphQL.

Consuming GraphQL API hosted on AppSync-AWS from Postman:

  • In this topic, we are going to see how we can consume GraphQL API hosted on AppSync Service in AWS from Postman.
  • Create post  request from Postman and enter the request URL as GraphQL API. It typically looks in the format as below:“https://{{UniqueId}}.appsyncapi.{{AWS_Region}}.amazonaws.com/graphql”
  • Choose the Authentication method as “AWS Signature”. Input the “AccessKey” and “SecretKey” which should be basically provided by your AWS consultant.
  • Select Advanced option. Input your region name in the field “AWS Region” and Input “appsync” in the field “Service Name”.

 

  • GraphQL works in a way where you will get the response based on what you give as input query. So, finalize the GraphQL query after understanding the GraphQL schema completely and input the same in the body of your request.
  • Select the body type as “GraphQL”.
  • Now, we are all set to post this message to AWS and let we see what we are getting as a response.

 

  • You can see in the above screenshot, we have got successful response code as well as the data.
  • As said earlier, you could observe that, the response will be in json structure enclosed with in “data” field which typically same as the query structure which we have given as input.

Consuming GraphQL API hosted on AppSync-AWS from SAP CPI:

  • We are going to follow the same procedure in CPI like we did in Postman. But, the Postman has the inbuilt capacity to calculate the “Host” and “AWS Signature” based on the inputs that we have given.
  • These two things need to be carried out exclusively in CPI to integrate the DynamoDB. I have achieved this via Groovy.
  • The sender http is to trigger an integration flow. In the content modifier, hardcode the  header as shown in the below screenshot.

 

  • GraphQL query needs to be passed in the body of the HTTP Post call which we can achieve it by hardcoding the same in body of the Content Modifier.
  • Note: Copy pasting the query which we have used in postman doesn’t work which will through a “Bad Request” error.
  • We need to preserve the alignment by adding the related the escape characters as shown in the below screenshot.
  • ·You can get this by try sending that GraphQL query to CPI endpoint and observe the same by enabling Trace. (This is Just a prerequisite to get to know on the related escape characters).
  • Now, you can use the above GraphQL query in the body of the content modifier by choosing the Type as “Constant”.
  • But, in my case, I haven’t used content modifier as I have got an need to pass the certain values in the query at the runtime.
  • Why not Content Modifier still with type as “Expression”?If we use expression, the escape symbol will be parsed which in turn leads to starting point. Hence, it will fail with “400 – Bad request” error.
  • So, I have gone to groovy to achieve the same.
  • Note: In groovy, you may have to add the escape (“\”) where ever the escape character was used to preserve its use.
  • Now, we are all set with respect to the input content. Our next step is to focus on the Authentication.
  • AWS supports only the AWS signature authentication which is not available by default in any of the adapter in CPI. So, we are going to calculate this AWS signature with reference to the below links through Groovy script and place it as authorization header.Reference:https://docs.aws.amazon.com/general/latest/gr/sigv4-create-canonical-request.html
  • Add a Groovy script and place the below code from snippet.
    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 = '/graphql';
        
        // 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';
        
        // 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';
        
        // 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);
        //message.setHeader("string_to_sign", string_to_sign);
        
        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;
    }
  • You can observe that, there were four inputs which were required predominantly in the groovy script to proceed with the further steps.
  • These four parameters is actually the same value what we have given in the postman. Create a secure parameter for each one respectively with the same naming conventions what you have declared in the groovy script.
  • Navigate to Monitoring Overview à Manage Security à Security Material à “Create” à “Secure Parameter”

 

  • Now, we are almost set. The related headers are created and AWS signature has been calculated and set as header in the groovy script.

  • Now, make a call to AWS DynamoDB via http adapter with the help of request reply. Then, deploy the IFlow and trigger from Postman to check whether the things are working fine.

 

 

  • *** While Testing the flow for first time, you may get into error as AWS DynamoDB host was not trusted by default in SAP CPI. In this case, download the certificate and add it in SAP CPI trust store.

Conclusion:

 

Be the first to leave a comment
You must be Logged on to comment or reply to a post.