Introduction#

GitHub this note shows how to access private data in CloudFront using signed URL and signed cookies. It also shows how to generate and get signed URL via NextJS route handler, and how to set cookies via NextJS server action.

  • Distribution and KeyGroup
  • NextJS API Route Handler
  • NextJS Server Actions
  • CloudFront Signed URl and Cookies

CloudFront Distribution#

  • Step 1 . Create a Origin Access Identity
const oai = new aws_cloudfront.CfnCloudFrontOriginAccessIdentity(
this,
"OriginAccessIdentityForSignedUrlDemo",
{
cloudFrontOriginAccessIdentityConfig: {
comment: "demo",
},
}
);
  • Step 1. Or Create Original Access Control (Recommended)
const oac = new aws_cloudfront.CfnOriginAccessControl(
this,
"OriginAccessControlForSignedUrlDemo",
{
originAccessControlConfig: {
name: "OriginAccessControlForSignedUrlDemo",
originAccessControlOriginType: "s3",
signingBehavior: "always",
signingProtocol: "sigv4",
},
}
);
  • Step 2. Create a key group and public key (use existing one)
const keygroup = new aws_cloudfront.CfnKeyGroup(
this,
"CloudFrontKeyGroupDemo",
{
keyGroupConfig: {
items: ["K3AAN213AO85TT"],
name: "CloudFrontKeyGroupDemo",
comment: "demo keygroup to generate signed url",
},
}
);
  • Step 3. Create a CloudFront Distribution using CDK level 1 stack
const dist = new aws_cloudfront.CfnDistribution(
this,
"CloudFrontDistributionSignedUrlDemo",
{
distributionConfig: {
// aliases: [],
enabled: true,
defaultCacheBehavior: {
targetOriginId: bucket.bucketDomainName,
viewerProtocolPolicy: "allow-all",
compress: true,
forwardedValues: {
queryString: false,
cookies: {
forward: "none",
// whitelistedNames: [""],
},
},
},
cacheBehaviors: [
{
pathPattern: "/private-data/*",
targetOriginId: bucket.bucketDomainName,
viewerProtocolPolicy: "allow-all",
allowedMethods: ["GET", "HEAD"],
// cachedMethods: [],
cachePolicyId: "658327ea-f89d-4fab-a63d-7e88639e58f6",
compress: true,
trustedKeyGroups: [keygroup.attrId],
// trustedSigners: [],
},
],
origins: [
{
domainName: bucket.bucketDomainName,
id: bucket.bucketDomainName,
// originAccessControlId: oac.attrId,
// originPath: "",
// originAccessControlId: "",
s3OriginConfig: {
originAccessIdentity: `origin-access-identity/cloudfront/${oai.attrId}`,
},
},
],
// s3Origin: {
// dnsName: bucket.bucketDomainName,
// originAccessIdentity: oai.ref,
// },
},
}
);
  • Step 4. S3 Bucket Policy grants OAI or OAC access
bucket.addToResourcePolicy(
new aws_iam.PolicyStatement({
actions: ["s3:GetObject", "s3:PutObject"],
resources: [bucket.arnForObjects("*")],
principals: [
new aws_iam.ArnPrincipal(
`arn:aws:iam::cloudfront:user/CloudFront Origin Access Identity ${oai.attrId}`
),
})
);

or with OAC

bucket.addToResourcePolicy(
new aws_iam.PolicyStatement({
actions: ["s3:GetObject", "s3:PutObject"],
resources: [bucket.arnForObjects("*")],
principals: [new aws_iam.ServicePrincipal("cloudfront.amazonaws.com")],
conditions: {
StringEquals: {
"AWS:SourceArn": `arn:aws:cloudfront::${this.account}:distribution/${dist.attrId}`,
},
},
})
);

API Route Handler#

Route handler can create APIs which handle at server side for different tasks. In this note, we will use it to generate signed url to access private data in CloudFront. It can set cookies also.

|--app
|--api
|--route.js
|--blog
|--page.tsx
|--page.tsx

Logic of the api/route.js handler to send back json response

import { NextResponse } from "next/server";
export async function GET(request) {
return NextResponse.json({ name: signedUrl });
}

The route handler also can set cookies

export async function GET(request) {
return new Response("Hello Hai Tran", {
status: 200,
headers: {
"Set-Cookie": [
"next_auth=deleted; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT",
"token=1234abcd",
],
},
});
}

Then we can call the api in blog page: call api => signed url => ref tag

const getPost = async () => {
const res = await fetch(process.env.API_URL as string);
const data = await res.json();
console.log("response data: ", data);
return data;
};
const BlogPage = async () => {
const result = await getPost();
return (
<div>
<h1>{result.name}</h1>
<a href={result.name} target="_blank">
<button className="px-10 py-2 bg-green-500">Get the Book</button>
</a>
</div>
);
};
export default BlogPage;

Server Action#

Route handler help reduce sending javascirpt to client, and server take care more tasks. Currently it support form action. It also can set cookies

import { cookies } from "next/headers";
export const BookPage = async () => {
async function getBook() {
// please pay attention to use server
"use server";
cookies().set("book", "1234");
}
return (
<form action={getBook}>
<button type="submit" className="px-10 py-2 bg-green-400 ">
Set Cookies
</button>
</form>
);
};
export default BookPage;

Signed URL#

To protect private data in CloudFront we need to

  • Create a key pair
  • Add behavior for a path pattern
  • Generate the signed URl

Step 1) create a pair key HERE. Our application use the private key to generate a signed url and CloudFormation use the public key to validate the signed url.

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

Step 2) Using nodejs we can create a signed url for a object in CloudFront as below

var cfsign = require("aws-cloudfront-sign");
var signingParams = {
keypairId: process.env.PUBLIC_KEY_ID,
privateKeyString: Buffer.from(process.env.PRIVATE_KEY, "base64").toString(
"ascii"
),
// expireTime: 1426625464599
};
var signedUrl = cfsign.getSignedUrl(
"https://${cloudfront-domain}/private-data/dolphin.png",
signingParams
);

Step 3) Goto AWS CloudFront console

  • Create a path behavior
  • Create a public key using the generated public key
  • Configure restrict access using signed url
/private-data/*

Reference#