Has anyone successfully used the AWS SDK to generate signed URLs to objects in an S3 bucket which also work over CloudFront? I'm using the JavaScript AWS SDK and it's really simple to generate signed URLs via the S3 links. I just created a private bucket and use the following code to generate the URL:
var AWS = require('aws-sdk') , s3 = new AWS.S3() , params = {Bucket: 'my-bucket', Key: 'path/to/key', Expiration: 20} s3.getSignedUrl('getObject', params, function (err, url) { console.log('Signed URL: ' + url) })
This works great but I also want to expose a CloudFront URL to my users so they can get the increased download speeds of using the CDN. I setup a CloudFront distribution which modified the bucket policy to allow access. However, after doing this any file could be accessed via the CloudFront URL and Amazon appeared to ignore the signature in my link. After reading some more on this I've seen that people generate a .pem file to get signed URLs working with CloudFront but why is this not necessary for S3? It seems like the getSignedUrl method simply does the signing with the AWS Secret Key and AWS Access Key. Has anyone gotten a setup like this working before?
Update: After further research it appears that CloudFront handles URL signatures completely different from S3 [link]. However, I'm still unclear as to how to create a signed CloudFront URL using Javascript.
S3 pre-signed URLs are a form of an S3 URL that temporarily grants restricted access to a single S3 object to perform a single operation — either PUT or GET — for a predefined time limit. To break it down: It is secure — the URL is signed using an AWS access key.
To create a CloudFront distributionOpen the CloudFront console at https://console.aws.amazon.com/cloudfront/v3/home . Choose Create Distribution, and then choose Get Started. Under Origin Settings, for Origin Domain Name, choose the Amazon S3 bucket that you created earlier.
To generate a presigned URL using the AWS Management ConsoleSign in to the AWS Management Console and open the Amazon S3 console at https://console.aws.amazon.com/s3/ . In the Buckets list, choose the name of the bucket that contains the object that you want a presigned URL for.
A signed URL will be produced for each provided URL, authorized for the specified HTTP method and valid for the given duration. The signurl command uses the private key for a service account (the '<private-key-file>' argument) to generate the cryptographic signature for the generated URL.
Update: I moved the signing functionality from the example code below into the aws-cloudfront-sign package on NPM. That way you can just require this package and call getSignedUrl()
.
After some further investigation I found a solution which is sort of a combo between this answer and a method I found in the Boto library. It is true that S3 URL signatures are handled differently than CloudFront URL signatures. If you just need to sign an S3 link then the example code in my initial question will work just fine for you. However, it gets a little more complicated if you want to generate signed URLs which utilize your CloudFront distribution. This is because CloudFront URL signatures are not currently supported in the AWS SDK so you have to create the signature on your own. In case you also need to do this, here are basic steps. I'll assume you already have an S3 bucket setup:
To great a signed CloudFront URL you just need to sign your policy using RSA-SHA1 and include it as a query param. You can find more on custom policies here but I've included a basic one in the sample code below that should get you up and running. The sample code is for Node.js but the process could be applied to any language.
var crypto = require('crypto') , fs = require('fs') , util = require('util') , moment = require('moment') , urlParse = require('url') , cloudfrontAccessKey = '<your-cloudfront-public-key>' , expiration = moment().add('seconds', 30) // epoch-expiration-time // Define your policy. var policy = { 'Statement': [{ 'Resource': 'http://<your-cloudfront-domain-name>/path/to/object', 'Condition': { 'DateLessThan': {'AWS:EpochTime': '<epoch-expiration-time>'}, } }] } // Now that you have your policy defined you can sign it like this: var sign = crypto.createSign('RSA-SHA1') , pem = fs.readFileSync('<path-to-cloudfront-private-key>') , key = pem.toString('ascii') sign.update(JSON.stringify(policy)) var signature = sign.sign(key, 'base64') // Finally, you build the URL with all of the required query params: var url = { host: '<your-cloudfront-domain-name>', protocol: 'http', pathname: '<path-to-s3-object>' } var params = { 'Key-Pair-Id=' + cloudfrontAccessKey, 'Expires=' + expiration, 'Signature=' + signature } var signedUrl = util.format('%s?%s', urlParse.format(url), params.join('&')) return signedUrl
For my code to work with Jason Sims's code, I also had to convert policy to base64 and add it to the final signedUrl, like this:
sign.update(JSON.stringify(policy)) var signature = sign.sign(key, 'base64') var policy_64 = new Buffer(JSON.stringify(policy)).toString('base64'); // ADDED // Finally, you build the URL with all of the required query params: var url = { host: '<your-cloudfront-domain-name>', protocol: 'http', pathname: '<path-to-s3-object>' } var params = { 'Key-Pair-Id=' + cloudfrontAccessKey, 'Expires=' + expiration, 'Signature=' + signature, 'Policy=' + policy_64 // ADDED }
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With