We are trying to access S3 resources via Cloudfront inside a VF page in Salesforce.

The S3 bucket, folder & files are all marked private and the access is granted only via Cloudfront.

We already tried generating the Signed url using the approach discussed in this thread in SFSE and also this option in SF Dev Forum

With both the options, we are getting 403 AccessDenied errors.

here's the current code we have in place.

public static String GetCFsignedURL() {
    //Private Key in RSA
    String cloudfrontPrivateKey1 = '.....';

    //Private Key in PKCS8
    String cloudfrontPrivateKey2 = '....';        

    String keyPairId = '...';
    String secret    = '...';

    Datetime dt      = Datetime.now();
    Long longTime    = dt.getTime();
    Long expiryLong  = (longTime / 1000) + 3600;
    String expiry    = String.valueOf(expiryLong);

    String strpolicy = '{"Statement": [{"Resource": "https://prefix.cloudfront.net/s3folder/s3File","Condition": {"DateLessThan": {"AWS:EpochTime": ' + expiry + '}}}]}';

    String policyEnc = EncodingUtil.base64Encode(Blob.valueOf(strpolicy));

    policyEnc = policyEnc.replace('+', '-').replace('=', '_').replace('/', '~');

    //Blob mac         = Crypto.Sign('RSA',Blob.valueOf(policyEnc), EncodingUtil.base64Decode(cloudfrontPrivateKey1));

    Blob mac = Crypto.generateMac('hmacSHA1', Blob.valueOf(rPolicy), Blob.valueOf(cloudfrontPrivateKey1));

    String policySign = EncodingUtil.base64Encode(mac);

    String finalURL = 'https://prefix.cloudfront.net/s3folder/s3File?Expires=' + expiry + '&Signature=' + policySign + '&Key-Pair-Id=' + keyPairId;

    return finalURL;

Note - When I generate the cloudfront signed url from a .NET or Java client using the same private keys & access key pair id, it perfectly works fine. Its failing only with the signed url generated in Salesforce.

if anyone got any luck with generating cloudfront signed url within apex, please share your expertise..

  • If you take a fixed DateTime for both the Apex and the .NET or Java code do they produce exactly the same URL output? May 27 '18 at 20:00
  • Hi @DanielBallinger the URL format from both the .NET, Java & Apex are all the same. I even took the expiry time from the .NET client and hardcoded in apex for a quick test but no luck. it definitely something wrong or missing in the signature generation part which I'm not able to figure out :-( May 27 '18 at 23:30
  • So the mac blob isn't what you required? I was thinking if you could provide sample values from the .NET or Java code and the expected outputs it would be possible to go through step by step to isolate the problem. May 27 '18 at 23:40

You may want to implement the string replacement code found in both of those answers you referenced. I've run into similar issues generating presigned URLs for S3 and learned the (very) hard way about URL escaping reserved characters in the policy. In your code, you may want to try something like this:

String policyEnc = EncodingUtil.base64Encode(Blob.valueOf(strpolicy));
policyEnc = policyEnc.replace('+','-').replace('=','_').replace('/','~');

Other languages (eg., Python in my case or .NET/Java in your case) seem to do a better job with this type of URL generation, but I've spent countless hours trying to debug this stuff in Apex in the past. Hope that helps!

Edit: just to be clear, the reason this should help (if not solve the issue) is because you're generating a presigned URL in the end, and that URL has to be properly formatted. If your base64-encoded policy includes characters normally found in URIs/URLs (eg., +, =, or /) it will fail.

  • Paul, I do have a function to replace those characters and the policy string is going thru that, but still it's the same issue. I will update the question with it. any other possible issue that might be causing the access denied error from cloudfront ? May 27 '18 at 7:25
  • Are you getting any more specific errors in the response about the policy, or is it simply a 403? To reduce the number of variables that could be causing problems, start with a very basic policy (eg., {"Statement": [{"Resource" : "https://*" , "Condition": {"DateLessThan": {"AWS:EpochTime": 1527529777}}}]}). Use a fixed time far enough in the future so you can generate identical URLs. Make the same changes to your Java/.NET clients that work, and compare the URLs that are generated to see which parts are different. If you share them here, we can take a look.
    – Paul
    May 27 '18 at 17:51
  • I tried that with the time part by taking the final value working from my .net client and it didn't work. but I didn't try with the generic resource part. will give that a try now. May 27 '18 at 23:26

Finally after multiple communications with SF and AWS (Cloudfront & JDK) support teams, we managed to successfully generate the cloudfront signed URL to access the private resource in S3..

here's the code snippet that worked for us..

//endpoint parameter - resource path inside the S3 folder
public static string getCFsignedURL(String endpoint) {
    //key pair id from S3
    String keyPairId = Key_Pair_Id__c;

    //the private key from pkcs8 file
    Blob privateKey = EncodingUtil.base64Decode(Private_Key__c);        

    //cloudfront full path to sign
    String urlToSign = Cloudfront_Domain__c + endpoint;

    //Expiry time
    Datetime dt      = Datetime.now();
    Long longTime    = dt.getTime();
    Long expiryLong  = (longTime / 1000) + 3600;
    String Expiry    = String.valueOf(expiryLong);

    //Policy Statement for the cloudfront full path with the expiry time
    String policyStatementText = '{"Statement":[{"Resource":"' + urlToSign + '","Condition":{"DateLessThan":{"AWS:EpochTime":' + Expiry + '}}}]}';

    String policyStatement = EncodingUtil.base64Encode(Blob.valueOf(policyStatementText ));
    policyStatement = policyStatement.replace('+','-');
    policyStatement = policyStatement.replace('=','_');
    policyStatement = policyStatement.replace('/','~');

    //genarate the signature
    Blob mac = Crypto.sign('RSA-SHA1', blob.valueof(policyStatementText),privateKey );
    String signed = EncodingUtil.base64Encode(mac);        
    signed = signed.replace('+','-');
    signed = signed.replace('=','_');
    signed = signed.replace('/','~');

    String signedURL = urlToSign + '?' + 'Policy=' + policyStatement + '&Signature=' + signed  + '&Key-Pair-Id=' + keyPairId ;

    return signedURL;

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.