Search code examples
amazon-web-servicescookiesamazon-cloudfront

Access Denied Error when using Cloudfront with Signed Cookies


I know this topic has been asked and answered multiple times all over the internet but unfortunately I'm still having issues although trying basically every single related post out there!

My setup

I have an S3 bucket with video files for a frontend streaming service. The files get served via a CloudFront Distribution and in order to secure the file access, I have set up an API Gateway which redirect requests to a Custom Lambda to get my Signed Cookies for CloudFront.

However, tests for the individual services were all successful:

  • Setting S3 access permissions to public and requesting the *.m3u8video manifest directly from the S3 works as expected (stream works like a charm).
  • After setting up a custom SSL certificate for my Cloudfront distribution (to avoid CORS), and requesting the same file via CloudFront also works.
  • Since my frontend needs access to an entire folder on the S3 (for streaming), the only way - at least as I understood - is to use signed cookies for the request. Therefore, I have set up a Custom Lambda which returns the Set-Cookie headers for the CloudFront domain.
  • For returning custom headers, we need to set up an API Gateway and link it to the lambda.

I want to emphasise, that S3, CloudFront, Lambda and the API Gateway are working individually but only as long as I don't attach the cookies to the request!

So my guess is that something is wrong with the signing of the cookies.

Custom Lambda

Since the lambda is the source of truth for cookie signing, I will show a minimal example of what I'm doing here. My lambda is written in Node JS and I'm using "@aws-sdk/cloudfront-signer": "^3.229.0",.

const url = `https://my-cloudfront.domian.com/path-to-src/*`;
const expTime = new Date(Date.now() + 5 * (60 * 60 * 1000)).toISOString();

const signedCookie = getSignedCookies({
  keyPairId: awsCloudfrontKeyPairId,    // from CloudFront setup
  privateKey: awsCloudfrontPrivateKey,  // from CloudFront setup
  url: url,
  dateLessThan: getExpTime,
});

const response = {
  statusCode: 200,
  isBase64Encoded: false,
  body: JSON.stringify({ url: url, bucket: bucket, key: key }),
  headers: {
    "Content-Type": "application/json",
    "Access-Control-Allow-Origin": "*",
    "Access-Control-Allow-Credentials": true,
    "Access-Control-Allow-Methods": "OPTIONS,POST,GET",
  },
  multiValueHeaders: {
    "Set-Cookie": [
      `CloudFront-Expires=${signedCookie["CloudFront-Expires"]}; Domain=domain.com; Path=/*`,
      `CloudFront-Signature=${signedCookie["CloudFront-Signature"]}; Domain=domain.com; Path=/*`,
      `CloudFront-Key-Pair-Id=${signedCookie["CloudFront-Key-Pair-Id"]}; Domain=domain.com; Path=/*`,
    ],
  },
};

callback(null, response);

This works, I get the 3 Set-Cookie headers which also get submitted when requesting the file. However, the result is always the same:

ACCESS DENIED

Tests

To test the individual services I did the following:

  • S3 only --> stream works
  • CloudFront without viewer restriction --> stream works
  • CloudFront with viewer restriction but no cookies --> Missing Key error
  • CloudFront with viewer restriction and cookies --> Access Denied error

There obviously is a lot more important setup steps (e.g. bucket policies, custom domains, etc), which I did not mention here but can be posted on request if necessary.

This topic keeps me busy since a month now! Any help appreciated! 🙌


Solution

  • I found the solution and it's pretty embarrassing...

    My above example signs cookies via a Custom Policy but uses the Set-Cookie header keys from a Canned Policy. The difference in the docs is not really obvious but its clearly there...

    However, instead of

    Set-Cookie: CloudFront-Expires= ...
    

    you use

    Set-Cookie: CloudFront-Policy=<your policy string>
    

    The keys

    Set-Cookie: CloudFront-Expires= ...
    Set-Cookie: CloudFront-Signature= ...
    

    are identical.

    Hope this helps!