Tuesday, February 11, 2020

Retrieve records from Amazon RDS through Salesforce Apex class

Motivation behind this


I was doing a proof-of-concept on retrieving records from Amazon RDS (Relational Database Service) where I have faced critical challenges in authorization mechanism with AWS Signature version 4 signing process which motivates me to write this post.

Usually, most of the cases I have come across either posting or retrieving files to/from AWS S3. But there can be much more.

Let's start with a use case.

Use Case


Business has a requirement to view the data in Salesforce where data is maintained in PostgreSQL database. Amazon RDS will host REST based webservice which retrieves the records from PostgreSQL database table. From Salesforce Apex class, it will make a callout to REST based end point and fetch the data as response.

The architecture looks like this:

For this use case, we will concentrate on the Apex part assuming we have an end point to connect to AWS.

Solution Approach


We will create a Apex class which makes the callout to the end point.


Flow diagram will look like this.


Flow Diagram


Flow diagram will show step by step approach of signing process to perform callout.



Broadly, the signing process can be divided into following 4 steps:

  • Create Canonical Request

  • Create String to Sign

  • Calculate Signature
  • Create Request Header and perform callout.

Code Sample


The guidance has been taken from Signature Version 4 Signing Process


  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
public class MyAWSService{

 //request values
 String region = 'us-east-1';
 String service = 'execute-api';
 String key = 'Provide AWS Key'; //AWS key
 String secret = 'Provide AWS Secret key'; //AWS Secret key
 String host = 'api.amazonaws.com'; 
 String stage = 'qa'; //dev or qa
 String serviceName = 'retrieveAccount'; //this actually fetches the data from PostgreSQL
 
 String method = 'GET';
 String request_parameters = 'Action=ListUsers&Version=2010-05-08';
 String canonical_querystring = '';

 String qaAPIKey = 'Provide API Key';
 
 //date for headers and the credential string
 String amzdate = Datetime.now().formatGMT('yyyyMMdd\'T\'HHmmss\'Z\'');
 String datestamp = Datetime.now().formatGMT('yyyyMMdd');
  
 public void runScript(){
  
  HttpRequest req = new HttpRequest();
  //set the request method (GET, PUT etc.)
  req.setMethod(method);
  
  //create end point URL
  String endPoint = 'https://' + host + '/' + stage + '/' + serviceName;
  system.debug('Endpoint='+endPoint); //'https://api.amazonaws.com/qa/retrieveAccount'

  //assign endpointURL to request
  req.setEndpoint(endPoint);  
  
  //************* TASK 1: CREATE A CANONICAL REQUEST *************
  //Step 1 is to define the verb (GET, POST, etc.)
  //Step 2: Create canonical URI--the part of the URI from domain to query 
  String canonical_uri = '/' + stage + '/' + serviceName;    // '/qa/retrieveAccount'
  
  /*Step 3: Create the canonical query string. In this example (a GET request),
                # request parameters are in the query string. Query string values must
                # be URL-encoded. The parameters must be sorted by name.
  */
  canonical_querystring = '';
  
  //Step 4: Create the canonical headers and signed headers.
  String canonical_headers = 'host:' + host + '\n' + 'x-amz-date:' + amzdate + '\n';
  System.debug('##canonical_headers:' + canonical_headers);
  
  //Step 5: Create the list of signed headers.
  String signed_headers = 'host;x-amz-date';
  
  //Step 6: Create payload hash (hash of the request body content). 
                //For GET requests, the payload is an empty string ('')
  Blob payload = Blob.valueOf('');
  String payload_hash = EncodingUtil.convertToHex(Crypto.generateDigest('SHA-256', payload));
  
  //Step 7: Combine elements to create create canonical request  
  String canonical_request = method + '\n' 
      + canonical_uri + '\n'  
      + canonical_querystring + '\n' 
      + canonical_headers + '\n' 
      + signed_headers + '\n' 
      + payload_hash;
        
  System.debug('canonical_request=' + canonical_request);

  //************* TASK 2: CREATE THE STRING TO SIGN*************
  String algorithm = 'AWS4-HMAC-SHA256';
  String credential_scope = datestamp + '/' + region + '/' + service + '/' + 'aws4_request';
  String string_to_sign = algorithm + '\n' +  amzdate + '\n' +  credential_scope + '\n' + 
        EncodingUtil.convertToHex(Crypto.generateDigest('sha256', Blob.valueOf(canonical_request)));
  System.debug('String_to_sign: ' + string_to_sign);

  //************* TASK 3: CALCULATE THE SIGNATURE *************
  //generate signing key
  Blob signingKey = createSigningKey(secret);
  
  //generate signature  
  String signature =  createSignature(string_to_sign, signingKey); 
  
  //************* TASK 4: ADD SIGNING INFORMATION TO THE REQUEST *************
  String authorization_header = algorithm + ' ' 
      + 'Credential=' + key + '/' 
      + credential_scope + ', ' 
      +  'SignedHeaders=' + signed_headers + ', ' 
      + 'Signature=' + signature;
       
  req.setHeader('Authorization',authorization_header);
  
  //The request can include any headers,
  req.setHeader('x-api-key', qaAPIKey);
  req.setHeader('x-amz-date', amzdate);
  req.setHeader('Accept', 'application/json');
  
  Http http = new Http();
  HTTPResponse res = http.send(req);
  System.debug('*Resp:' + String.ValueOF(res.getBody()));
  System.debug('RESPONSE STRING: ' + res.toString());
  System.debug('RESPONSE STATUS: ' + res.getStatus());
  System.debug('STATUS_CODE: ' + res.getStatusCode()); 
 
 }

 //key derivation functions
 private Blob createSigningKey(String secretKey){
        Blob dateKey = signString(Blob.valueOf(datestamp),Blob.valueOf('AWS4'+secretKey));
        Blob dateRegionKey = signString(Blob.valueOf(region),dateKey);
        Blob dateRegionServiceKey = signString(Blob.valueOf(service),dateRegionKey);
        return signString(Blob.valueOf('aws4_request'),dateRegionServiceKey);
    }

 private Blob signString(Blob msg, Blob key){
        return Crypto.generateMac('HMACSHA256', msg, key);
    } 
 
 private String createSignature(String stringToSign, Blob signingKey){        
  return EncodingUtil.convertToHex(Crypto.generateMac('HMACSHA256', blob.valueof(stringToSign), signingKey));
    }
}

Few points to be noted:


  • Service has been used as 'execute-api'

  • We can use same code for other HTTP method like, POST, PATCH etc.

  • For GET method payload should be empty.

  • Query parameter keys must be sorted.

  • Algorithm has been used as AWS4-HMAC-SHA256 for signing process.

  • Minimal request header parameters to be passed as 'x-api-key', 'x-amz-date', 'Accept'


If we don't follow the step properly then most of the cases it throws Signature doesn't match error.


Also, Troubleshooting AWS Signature Version 4 Errors helps to solve the issues.


It has taken few days to implement expected authorization mechanism. 


Hope it helps!



Further Reading


No comments:

Post a Comment