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'