I am attempting to access s3 from Apex using credentials returned from AssumeRole. However, I am receiving the following error:
<Message>The AWS Access Key Id you provided does not exist in our records.</Message>
<AWSAccessKeyId>ASIA********</AWSAccessKeyId>
I am able to successfully call GetObject on this s3 bucket from the CLI using the credentials returned from AssumeRole, so I can be reasonably sure that my bucket permissions have been set up fine. I have the following code in Apex:
Http http = new http();
Profile p = [SELECT Id FROM Profile WHERE Profile.Name = 'S3 Test User' LIMIT 1];
S3_Settings__c s3 = S3_Settings__c.getInstance(p.Id);
String exp = String.valueOf(Cache.Session.get('expiration'));
String sessionToken = String.valueOf(Cache.Session.get('token'));
if(exp == null || exp == '' || (DateTime) JSON.deserialize('"' + exp + '"', DateTime.class) < System.now()) {
requestSessionToken();
}
sessionToken = String.valueOf(Cache.Session.get('token'));
DateTime expires = (DateTime) JSON.deserialize('"' + String.valueOf(Cache.Session.get('expiration')) + '"', DateTime.class);
String accessKeyId = String.valueOf(Cache.Session.get('accessKeyId'));
String accessSecret = String.valueOf(Cache.Session.get('secret'));
String bucketname = s3.Recording_Bucket__c;
String host = 's3.amazonaws.com';
String formattedDateString = Datetime.now().formatGMT('EEE, dd MMM yyyy HH:mm:ss z');
String method = 'GET';
String filePath = 'https://' + bucketname + '.' + host + '/' + filename;
HttpRequest req = new HttpRequest();
req.setMethod(method);
req.setEndpoint(filePath);
req.setHeader('Host', bucketname + '.' + host);
req.setHeader('Connection', 'keep-alive');
String stringToSign = 'GET\n\n' + 'x-amz-security-token=' + sessionToken + '&expiration=' + expires + '\n' + formattedDateString + '\n/' + '/' + bucketname + '/' + filename;
System.debug('SIGN ' + stringToSign);
String encodedStringToSign = EncodingUtil.urlEncode(stringToSign, 'UTF-8');
Blob mac = Crypto.generateMac('HMACSHA1', blob.valueof(stringToSign),blob.valueof(accessSecret));
String signedKey = EncodingUtil.base64Encode(mac);
String authHeader = 'AWS' + ' ' + accessKeyId + ':' + signedKey;
req.setHeader('Date', formattedDateString);
//req.setHeader('x-amz-security-token', sessionToken); //AWS returns 'invalid signature' if this is set
req.setHeader('Authorization',authHeader);
HttpResponse resp = http.send(req);
It seems as if the AWS is reading in the AccessKeyId/Secret, but not the session token. I've also tried setting x-amz-security-token as a header, but this throws a 403 Error -- Signature mismatch. Am I missing something within my headers or signature that would enable this request to return successfully?
Turns out I was placing the x-amz-security-token header in the wrong location. It needs to occur in the canonical AMZ Headers section immediately after the formatted date, with a comma to separate the name and value:
String stringToSign = 'GET\n\n\n' + formattedDateString + '\n' + 'x-amz-security-token:' + sessionToken + '\n' + '/' + bucketname + '/' + filename;
Additionally, the following line needed to be uncommented:
req.setHeader('x-amz-security-token', sessionToken);
As a last note, be sure that neither the header nor the canonicalized AMZ Header are capitalized.