CloudFront Signed Cookies [NodeJS,iOS,Android,React]

Nipun Ruwanpathirana
7 min readDec 4, 2020

In this short tutorial, you will learn how to use AWS CloudFront Signed Cookies with NodeJS as the server and iOS as clients.

Note: React, iOS and Android implementation will be available soon

Setup CloudFront S3 bucket

Create a key pair for a trusted key group (recommended)

To create a key pair for a trusted key group, perform the following steps:

Create the public–private key pair.

The following example command uses OpenSSL to generate an RSA key pair with a length of 2048 bits and save to the file named private_key.pem.

openssl genrsa -out private_key.pem 2048

The resulting file contains both the public and the private key. The following example command extracts the public key from the file named private_key.pem.

openssl rsa -pubout -in private_key.pem -out public_key.pem

Upload the public key to CloudFront.

Add the public key to a CloudFront key group.

Origin Access Identity

Create S3 Bucket

  1. Sign in to the AWS Management Console and open the Amazon S3 console at https://console.aws.amazon.com/s3/.
  2. In the Amazon S3 console, click Create Bucket.
  3. In the Create Bucket dialog box, enter a bucket name. If you want to create separate input and output buckets, give the bucket an appropriate name.
  4. Select a region for your bucket. By default, Amazon S3 creates buckets in the US Standard region. We recommend that you choose a region close to you to optimize latency, minimize costs, or to address regulatory requirements. This is also the region in which you want Elastic Transcoder to do the transcoding.
  5. Click Create.

Create CloudFront Distribution

Sign in to the AWS Management Console and open the Amazon CloudFront console at https://console.aws.amazon.com/cloudfront/. and then create a web distribution.

Select your bucket in “Origin Domain Name” and Origin Access Identity

Make below changes.

Select the key group that we created.

Make below changes

Then create the distribution. Now your CloudFront distribution is ready

Update S3 permissions

In your S3 bucket, you have to set the policy and CORS like below.

Bucket policy

{
"Version": "2012-10-17",
"Id": "Policy1605776490887",
"Statement": [
{
"Sid": "Stmt1605776489182",
"Effect": "Allow",
"Principal": {
"AWS": [
"arn:aws:iam::cloudfront:user/CloudFront Origin Access Identity <Key>" ]
},
"Action": [
"s3:DeleteObject",
"s3:GetObject",
"s3:PutObject"
],
"Resource": "arn:aws:s3:::xxx/*"
}
]
}

CORS

[
{
"AllowedHeaders": [
"<base_url>"
],
"AllowedMethods": [
"PUT",
"POST",
"DELETE",
"GET",
"HEAD"
],
"AllowedOrigins": [
"*"
],
"ExposeHeaders": [],
"MaxAgeSeconds": 3000
}
]

Server (NodeJS)

After configuring the CloudFront, you can generate signed cookies using aws-sdk like below.

const AWS = require('aws-sdk');
const CONFIG = require('../config/config');
const {log} = require('./log.service');

class CloudFrontService {
constructor() {
/**
* AWS Config
*/

let keyPairId = CONFIG.cloudfront_key_pair_id;
let encodedPrivateKey = CONFIG.cloudfront_private_key;

let privateKey = (new Buffer(encodedPrivateKey, 'base64')).toString('utf8');

this.cloudFront = new AWS.CloudFront.Signer(
keyPairId,
privateKey
);

}

getSignedCookies() {
let expireTime = new Date().addHours(CONFIG.cloudfront_cookie_expires);
try {
const policy = JSON.stringify({
Statement: [
{
Resource: CONFIG.cloudfront_path,
Condition: {
DateLessThan: {
'AWS:EpochTime': expireTime.getUTCTime(),
},
},
},
],
});

return this.cloudFront.getSignedCookie({
policy,
});
} catch (e) {
log.error("Cookie Generation Error: ", e);
return null;
}
}
}


class Singleton {
constructor() {
if (!Singleton.instance) {
Singleton.instance = new CloudFrontService();
}
}

getInstance() {
return Singleton.instance;
}
}

module.exports = Singleton;

In your controller

let cookie = new CloudFrontService().getInstance().
getSignedCookies();

iOS (Swift)

This is a class called CookieManager which manages CloudFront signed cookies in a iOS app. The class has a shared instance to ensure only one instance of it exists throughout the app.

The class has three methods: initCookies, getCookies, and isCookieValid. initCookies is a private method that fetches the CloudFront cookies from the backend API and saves them to the user defaults. getCookies method is a public method that returns the saved cookies if they are still valid and not expired. If the cookies are expired or isForce is set to true, the method fetches new cookies by calling initCookies. isCookieValid method checks if the saved cookies are still valid and not expired.

The class also uses a UserDefaultManager to save and retrieve the cookies and their expire time. The cookies are returned as a CookieData struct which contains the CloudFront policy, signature, and key pair IDs. If there is an error fetching the cookies from the API or there are no saved cookies in the user defaults, the methods return nil. The class also logs errors using LogManager

class CookieManager {

static let shared = CookieManager()

private func initCookies(completion:@escaping (CookieData?) -> Void) {
UserServiceClient.getCookies { response in
switch response {
case .success(let result):
if let cookie = result.cookies, let time = result.expireTime {
UserDefaultManager.setValue(key: UserDefaultManager.keyCloudFrontPolicyKey, value: cookie.cloudFrontPolicyKey ?? "")
UserDefaultManager.setValue(key: UserDefaultManager.keyCloudFrontPolicyValue, value: cookie.cloudFrontPolicyValue ?? "")
UserDefaultManager.setValue(key: UserDefaultManager.keyCloudFrontSignatureKey, value: cookie.cloudFrontSignatureKey ?? "")
UserDefaultManager.setValue(key: UserDefaultManager.keyCloudFrontSignatureValue, value: cookie.cloudFrontSignatureValue ?? "")
UserDefaultManager.setValue(key: UserDefaultManager.keyCloudFrontKeyPairIdKey, value: cookie.cloudFrontKeyPairIdKey ?? "")
UserDefaultManager.setValue(key: UserDefaultManager.keyCloudFrontKeyPairIdValue, value: cookie.cloudFrontKeyPairIdValue ?? "")

UserDefaultManager.setValue(key: UserDefaultManager.keyCloudFrontExpireTime, value: Int(time) ?? 0)
completion(cookie)
} else {
LogManager.log(LogManager.error, data: "Error in getting cookies")
completion(nil)
}
case .failure(let error):
LogManager.log(LogManager.error, data: error)
completion(nil)
}
}
}

func getCookies(isForce: Bool = false, completion:@escaping (CookieData?) -> Void) {
if isForce {
initCookies { cookie in
completion(cookie)
}
}

if let currentCookieExpireTime = UserDefaultManager.getIntValue(key: UserDefaultManager.keyCloudFrontExpireTime) {
let now = Date().millisecondsSince1970
if currentCookieExpireTime < now {
//Expired. Requesting new
initCookies { cookie in
completion(cookie)
}
} else {
//Return current cookies
completion(CookieData(
cloudFrontPolicyKey: UserDefaultManager.getStringValue(key: UserDefaultManager.keyCloudFrontPolicyKey),
cloudFrontPolicyValue: UserDefaultManager.getStringValue(key: UserDefaultManager.keyCloudFrontPolicyValue),
cloudFrontSignatureKey: UserDefaultManager.getStringValue(key: UserDefaultManager.keyCloudFrontSignatureKey),
cloudFrontSignatureValue: UserDefaultManager.getStringValue(key: UserDefaultManager.keyCloudFrontSignatureValue),
cloudFrontKeyPairIdKey: UserDefaultManager.getStringValue(key: UserDefaultManager.keyCloudFrontKeyPairIdKey),
cloudFrontKeyPairIdValue: UserDefaultManager.getStringValue(key: UserDefaultManager.keyCloudFrontKeyPairIdValue))
)
}
} else {
initCookies { cookie in
completion(cookie)
}
}
}

func isCookieValid() -> Bool {
if let currentCookieExpireTime = UserDefaultManager.getIntValue(key: UserDefaultManager.keyCloudFrontExpireTime) {
let now = Date().millisecondsSince1970
if currentCookieExpireTime > now {
return true
}
}
return false
}
}

When downloading use this function to set cookies. This is a private function called getImageCookies that takes in a CookieData object as input parameter and returns an AnyModifier. The purpose of this function is to generate an AnyModifier object that can be used to modify HTTP requests and attach cookies to them.

Inside the function, a URL is constructed based on the ServerParam.cloudfrontURL value, which is a URL string. Three HTTP cookies are then generated using the HTTPCookie.cookies(withResponseHeaderFields:for:) method, passing in a dictionary of header fields that include the cookie key and value retrieved from the input CookieData object. The three cookies correspond to the cloudFrontPolicyKey, cloudFrontSignatureKey, and cloudFrontKeyPairIdKey values in the CookieData object.

The HTTPCookie.requestHeaderFields(with:) method is then called with the array of cookies generated above to obtain the headers that should be attached to the HTTP request. Finally, the function returns an AnyModifier object that modifies the input request by adding the headers obtained above and setting httpShouldHandleCookies to true, indicating that cookies should be automatically handled by the HTTP request.

private func getImageCookies(cookie: CookieData) -> AnyModifier {
let cookieUrl = URL(string: "\(ServerParam.cloudfrontURL!)/*")!

let modifier = AnyModifier { request in
var req = request
var httpCookies = [HTTPCookie]()

let cloudFrontPolicy = ["Set-Cookie": "\(cookie.cloudFrontPolicyKey ?? "")=\(cookie.cloudFrontPolicyValue ?? "")"]
let cookieCloudFrontPolicy = HTTPCookie.cookies(withResponseHeaderFields: cloudFrontPolicy, for: cookieUrl)
httpCookies.append(contentsOf: cookieCloudFrontPolicy)

let cloudFrontSignature = ["Set-Cookie": "\(cookie.cloudFrontSignatureKey ?? "")=\(cookie.cloudFrontSignatureValue ?? "")"]
let cookieCloudFrontSignature = HTTPCookie.cookies(withResponseHeaderFields: cloudFrontSignature, for: cookieUrl)
httpCookies.append(contentsOf: cookieCloudFrontSignature)

let cloudFrontKeyPairId = ["Set-Cookie": "\(cookie.cloudFrontKeyPairIdKey ?? "")=\(cookie.cloudFrontKeyPairIdValue ?? "")"]
let cookieCloudFrontKeyPairId = HTTPCookie.cookies(withResponseHeaderFields: cloudFrontKeyPairId, for: cookieUrl)
httpCookies.append(contentsOf: cookieCloudFrontKeyPairId)

let headers = HTTPCookie.requestHeaderFields(with: httpCookies)
req.httpShouldHandleCookies = true
req.allHTTPHeaderFields = headers
return req
}

return modifier
}

The code above is part of an implementation that fetches an image from a server and sets it to an image view. It makes use of CloudFront signed cookies to authenticate and authorize the request.

The CookieManager class is responsible for managing the CloudFront signed cookies. The shared property of this class is a singleton instance that can be used to obtain and manage the cookies.

The method CookieManager.shared.getCookies is called to retrieve the cookies from the server. The method takes a completion handler as a parameter, which will be called once the cookies are obtained.

If the cookies are obtained successfully, the getImageCookies method is called to create an AnyModifier instance that adds the cookies to the request headers of the image request. This modifier is then passed to the kf.setImage method of the Kingfisher library, which is used to fetch the image from the server and set it to the image view.

CookieManager.shared.getCookies { cookie in
guard nil != cookie else {
return
}

let modifier = self.getImageCookies(cookie: cookie!)
imageView?.kf.setImage(with: originalUrl, options: [.requestModifier(modifier)])
}

Thank you for reading.

--

--