21
votes

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.

3
I dont get your implementation. Can u explain it in detail. I need to do same for my android App.So what i have thought is this: My client will send a request to my server--> my server will request AWS S3 for signed URL-->My server will send this URL back to client to complete the uploading part. Is this fine approach. My server is written in node.js. Can u plz help me doing this. ?Abhishek Kaushik
If it's written in node you should be able to use the aws-cloudfront-sign utility I wrote. The README.md there should explain what you need to do.Jason Sims

3 Answers

24
votes

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:

Configure CloudFront

  1. Create a CloudFront distribution
  2. Configure your origin with the following settings
    • Origin Domain Name: {your-s3-bucket}
    • Restrict Bucket Access: Yes
    • Grant Read Permissions on Bucket: Yes, Update Bucket Policy
  3. Create CloudFront Key Pair. Should be able to do this here.

Create Signed CloudFront URL

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
2
votes

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 
}
1
votes

AWS includes some built in classes and structures to assist in the creation of signed URLs and Cookies for CloudFront. I utilized these alongside the excellent answer by Jason Sims to get it working in a slightly different pattern (which appears to be very similar to the NPM package he created).

Namely, the AWS.CloudFront.Signer type description which abstracts the process of creating signed URLs and Cookies.

export class Signer {
    /**
     * A signer object can be used to generate signed URLs and cookies for granting access to content on restricted CloudFront distributions.
     * 
     * @param {string} keyPairId - The ID of the CloudFront key pair being used.
     * @param {string} privateKey - A private key in RSA format.
     */
    constructor(keyPairId: string, privateKey: string);

    ....
}

And either an options with a policy JSON string or without a policy with a url and expiration time.

export interface SignerOptionsWithPolicy {
    /**
     * A CloudFront JSON policy. Required unless you pass in a url and an expiry time. 
     */
    policy: string;
}
export interface SignerOptionsWithoutPolicy {
    /**
     * The URL to which the signature will grant access. Required unless you pass in a full policy.
     */
    url: string
    /**
     * A Unix UTC timestamp indicating when the signature should expire. Required unless you pass in a full policy.
     */
    expires: number
}

Sample implementation:

import aws, { CloudFront } from 'aws-sdk';

export async function getSignedUrl() {

    // https://abc.cloudfront.net/my-resource.jpg
    const url = <cloud front url/resource>;

    // Create signer object - requires a public key id and private key value
    const signer = new CloudFront.Signer(<public-key-id>, <private-key>);

    // Setup expiration time (one hour in the future, in this case)
    const expiration = new Date();
    expiration.setTime(expiration.getTime() + 1000 * 60 * 60);
    const expirationEpoch = expiration.valueOf();

    // Set options (Without policy in this example, but a JSON policy string can be substituted)
    const options = {
        url: url,
        expires: expirationEpoch
    };

    return new Promise((resolve, reject) => {
        // Call getSignedUrl passing in options, to be handled either by callback or synchronously without callback
        signer.getSignedUrl(options, (err, url) => {
            if (err) {
                console.error(err.stack);
                reject(err);
            }
            resolve(url);
        });
    });
}