'Invalid AWS Access Key when calling S3 from Apex

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?



Solution 1:[1]

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.

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Casey Sheehan