CloudFront Signed Cookies [NodeJS,iOS,Android,React]
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
- Sign in to the AWS Management Console and open the Amazon S3 console at https://console.aws.amazon.com/s3/.
- In the Amazon S3 console, click Create Bucket.
- 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.
- 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.
- 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)])
}