Skip to content

Instantly share code, notes, and snippets.

@mekyush
Forked from patrick-fischer/AWS.cls
Last active June 17, 2020 16:28
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save mekyush/0eb95e74ef3528ac1aca431f4f85d72c to your computer and use it in GitHub Desktop.
Save mekyush/0eb95e74ef3528ac1aca431f4f85d72c to your computer and use it in GitHub Desktop.
AWS Authentication using Query Parameters (AWS Signature Version 4): https://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-query-string-auth.html
/**
// Example implementation as follows:
public class AWSGetService extends AWS {
public AWSGetService() {
super();
}
public override void init() {
endpoint = new Url('https://XXXXXXXX.execute-api.us-east-1.amazonaws.com/Test');
resource = '/';
region = 'us-east-1';
service = 'execute-api';
accessKey = 'access-key';
method = HttpMethod.XPOST;
// You can specify "payload" here if a body is required
payload = Blob.valueof('{"testbody":"abc"}');
// This method helps prevent leaking secret key,
// as it is never serialized
createSigningKey('secret-key');
// sendRequest();
}
public String[] getBuckets() {
HttpResponse response = sendRequest();
System.debug(response.getStatusCode());
System.debug(response.getBody());
System.debug(response);
String[] results = new String[0];
// Read response XML; if we get this far, no exception happened
// This code was omitted for brevity
return results;
}
}
*/
public abstract class AWS {
// Post initialization logic (after constructor, before call)
protected abstract void init();
// XML Node utility methods that will help read elements
public static Boolean getChildNodeBoolean(Dom.XmlNode node, String ns, String name) {
try {
return Boolean.valueOf(node.getChildElement(name, ns).getText());
} catch(Exception e) {
return null;
}
}
public static DateTime getChildNodeDateTime(Dom.XmlNode node, String ns, String name) {
try {
return (DateTime)JSON.deserialize(node.getChildElement(name, ns).getText(), DateTime.class);
} catch(Exception e) {
return null;
}
}
public static Integer getChildNodeInteger(Dom.XmlNode node, String ns, String name) {
try {
return Integer.valueOf(node.getChildElement(name, ns).getText());
} catch(Exception e) {
return null;
}
}
public static String getChildNodeText(Dom.XmlNode node, String ns, String name) {
try {
return node.getChildElement(name, ns).getText();
} catch(Exception e) {
return null;
}
}
// Turns an Amazon exception into something we can present to the user/catch
public class ServiceException extends Exception {
public String Code, Message, Resource, RequestId;
public ServiceException(Dom.XmlNode node) {
String ns = node.getNamespace();
Code = getChildNodeText(node, ns, 'Code');
Message = getChildNodeText(node, ns, 'Message');
Resource = getChildNodeText(node, ns, 'Resource');
RequestId = getChildNodeText(node, ns, 'RequestId');
}
public String toString() {
return JSON.serialize(this);
}
}
// Things we need to know about the service. Set these values in init()
protected String host, region, service, resource, accessKey, payloadSha256;
protected Url endpoint;
protected HttpMethod method;
protected Blob payload;
// Not used externally, so we hide these values
Blob signingKey;
DateTime requestTime;
Map<String, String> queryParams, headerParams;
// Make sure we can't misspell methods
public enum HttpMethod { XGET, XPUT, XHEAD, XOPTIONS, XDELETE, XPOST }
// Add a header
protected void setHeader(String key, String value) {
headerParams.put(key.toLowerCase(), value);
}
// Add a query param
protected void setQueryParam(String key, String value) {
queryParams.put(key.toLowerCase(), uriEncode(value));
}
// Call this constructor with super() in subclasses
protected AWS() {
requestTime = DateTime.now();
queryParams = new Map<String, String>();
headerParams = new Map<String, String>();
payload = Blob.valueOf('');
}
// Create a canonical query string (used during signing)
String createCanonicalQueryString() {
String[] results = new String[0], keys = new List<String>(queryParams.keySet());
keys.sort();
for(String key: keys) {
results.add(key+'='+queryParams.get(key));
}
return String.join(results, '&');
}
// Create the canonical headers (used for signing)
String createCanonicalHeaders(String[] keys) {
keys.addAll(headerParams.keySet());
keys.sort();
String[] results = new String[0];
for(String key: keys) {
results.add(key+':'+headerParams.get(key));
}
return String.join(results, '\n')+'\n';
}
// Create the entire canonical request
String createCanonicalRequest(String[] headerKeys) {
return String.join(
new String[] {
method.name().removeStart('X'), // METHOD
new Url(endPoint, resource).getPath(), // RESOURCE
createCanonicalQueryString(), // CANONICAL QUERY STRING
createCanonicalHeaders(headerKeys), // CANONICAL HEADERS
String.join(headerKeys, ';'), // SIGNED HEADERS
payloadSha256 // SHA256 PAYLOAD
},
'\n'
);
}
// We have to replace ~ and " " correctly, or we'll break AWS on those two characters
protected string uriEncode(String value) {
return value==null? null: EncodingUtil.urlEncode(value, 'utf-8').replaceAll('%7E','~').replaceAll('\\+','%20');
}
// Create the entire string to sign
String createStringToSign(String[] signedHeaders) {
String result = createCanonicalRequest(signedHeaders);
SYstem.debug('result in createStringToSign : ' + result);
return String.join(
new String[] {
'AWS4-HMAC-SHA256',
requestTime.formatGMT('YYYYMMdd\'T\'HHmmss\'Z\''),
String.join(new String[] { requestTime.formatGMT('YYYYMMdd'), region, service, 'aws4_request' },'/'),
EncodingUtil.convertToHex(Crypto.generateDigest('SHA256', Blob.valueof(result)))
},
'\n'
);
}
// Create our signing key
protected void createSigningKey(String secretKey) {
signingKey = Crypto.generateMac('hmacSHA256', Blob.valueOf('aws4_request'),
Crypto.generateMac('hmacSHA256', Blob.valueOf(service),
Crypto.generateMac('hmacSHA256', Blob.valueOf(region),
Crypto.generateMac('hmacSHA256', Blob.valueOf(requestTime.formatGMT('YYYYMMdd')), Blob.valueOf('AWS4'+secretKey))
)
)
);
}
// Create all of the bits and pieces using all utility functions above
HttpRequest createRequest() {
init();
payloadSha256 = EncodingUtil.convertToHex(Crypto.generateDigest('sha-256', payload));
setHeader('X-Amz-Content-Sha256', payloadSha256);
setHeader('X-Amz-Date', requestTime.formatGmt('YYYYMMdd\'T\'HHmmss\'Z\''));
if(host == null) {
host = endpoint.getHost();
}
setHeader('Host', host);
setHeader('Content-Type', 'application/json');
setHeader('org-id', 'XXXXXXXXX');
HttpRequest request = new HttpRequest();
request.setMethod(method.name().removeStart('X'));
if(payload.size() > 0) {
setHeader('Content-Length', String.valueOf(payload.size()));
request.setBodyAsBlob(payload);
}
String
finalEndpoint = new Url(endpoint, resource).toExternalForm(),
queryString = createCanonicalQueryString();
if(queryString != '') {
finalEndpoint += '?'+queryString;
}
request.setEndpoint(finalEndpoint);
for(String key: headerParams.keySet()) {
request.setHeader(key, headerParams.get(key));
}
String[] headerKeys = new String[0];
SYstem.debug('@@ headerKeys : ' + headerKeys);
String stringToSign = createStringToSign(headerKeys);
SYstem.debug('@@ stringToSign : ' +stringToSign);
request.setHeader(
'Authorization',
String.format(
'AWS4-HMAC-SHA256 Credential={0},SignedHeaders={1},Signature={2}',
new String[] {
String.join(new String[] { accessKey, requestTime.formatGMT('YYYYMMdd'), region, service, 'aws4_request' },'/'),
String.join(headerKeys,';'), EncodingUtil.convertToHex(Crypto.generateMac('hmacSHA256', Blob.valueOf(stringToSign), signingKey))}
));
System.debug('Authorization : ' + request.getHeader('Authorization'));
System.debug('X-Amz-Content-Sha256 : ' + request.getHeader('x-amz-content-sha256'));
System.debug('X-Amz-Date : ' + request.getHeader('x-amz-date'));
System.debug('Host : ' + request.getHeader('host'));
System.debug('Content-Type : ' + request.getHeader('content-type'));
System.debug('Content-Length : ' + request.getHeader('content-length'));
return request;
}
// Actually perform the request, and throw exception if response code is not valid
protected HttpResponse sendRequest(Set<Integer> validCodes) {
HttpResponse response = new Http().send(createRequest());
// if(!validCodes.contains(response.getStatusCode())) {
// throw new ServiceException(response.getBodyDocument().getRootElement());
// }
return response;
}
// Same as above, but assume that only 200 is valid
// This method exists because most of the time, 200 is what we expect
protected HttpResponse sendRequest() {
return sendRequest(new Set<Integer> { 200 });
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment