git clone https://github.com/amcquistan/aws-cdk-presigned-url-rest-api.git
cd aws-cdk-presigned-url-rest-api
openssl genrsa -out private_key.pem 2048
openssl pkcs8 -topk8 -nocrypt -in private_key.pem -inform PEM -out private_key.der -outform DER
openssl rsa -pubout -in private_key.pem -out public_key.pem
export PRIVATE_KEY=$(cat private_key.pem)
export PUBLIC_KEY=$(cat public_key.pem)
aws ssm put-parameter \
--name '/tci/demos/cloudfront/signedurls/privatekey' \
--value $PRIVATE_KEY \
--type SecureString
aws ssm get-parameter \
--name '/tci/demos/cloudfront/signedurls/privatekey' \
--with-decryption
cdk bootstrap YOUR_AWS_ACCOUNT_ID/YOUR_AWS_REGION
npm run build
npm run deploy
✅ AwsCdkPresignedUrlRestApiStack
Outputs:
AwsCdkPresignedUrlRestApiStack.BucketName = awscdkpresignedurlrestapist-randofilesbkt3d6ff9c2-p2h3td503nh1
AwsCdkPresignedUrlRestApiStack.RestApiUrl = https://l4my74gn8b.execute-api.us-east-1.amazonaws.com/v1/
echo "I'm the first test file" > testfile1.txt
echo "I'm the second test file" > testfile2.txt
aws s3 cp testfile1.txt s3://awscdkpresignedurlrestapist-randofilesbkt3d6ff9c2-p2h3td503nh1
aws s3 cp testfile2.txt s3://awscdkpresignedurlrestapist-randofilesbkt3d6ff9c2-p2h3td503nh1
http https://l4my74gn8b.execute-api.us-east-1.amazonaws.com/v1/s3objects -b
[
{
"ETag": "\"5472b570c2e878e3adc891451b8d21d9\"",
"Key": "testfile1.txt",
"LastModified": "2022-03-31T22:40:01.000Z",
"Size": 24,
"StorageClass": "STANDARD"
},
{
"ETag": "\"92c9265c51ade84a8bbc76f902e6f6ac\"",
"Key": "testfile2.txt",
"LastModified": "2022-03-31T22:40:12.000Z",
"Size": 25,
"StorageClass": "STANDARD"
}
]
http POST https://l4my74gn8b.execute-api.us-east-1.amazonaws.com/v1/urls file="testfile2.txt" -b
{
"signedUrl": "https://d3cvlqayyqxutd.cloudfront.net/testfile2.txt?Expires=1648807583&Key-Pair-Id=K1GJV5BZ7BHPXY&Signature=Idz8HwAIU41fUoVyrMms8nhHHlzXm8bzup6RViQuxCxBpYEqzAjEe~8aVxqrAr5v2HSyqx7DKxQSfiS6Adxq9R~PfQK-gcf3PoOfOuE-Gafgx2XkjUpqNspkS1fFBAx-i-PdAg8fn2lDHRAhneB2LKOaQJ3OtraPQssdSufa60x6lzAOxwwRTLemexBIFKb32ff8nGAjsoib8Y0~sbruTrCbZvShO~2VGkLOH7hyoBIPnvA~mhPExl-cBc7wUeYL0eNxqLuX-xLDlgnmPYV3glmqwcMTTvtlUhUk~V1vQofQjazisDWxDe2kq4hDKrIMVekRJHRgh3QIHYoiZos14g__"
}
http "https://d3cvlqayyqxutd.cloudfront.net/testfile2.txt?Expires=1648807583&Key-Pair-Id=K1GJV5BZ7BHPXY&Signature=Idz8HwAIU41fUoVyrMms8nhHHlzXm8bzup6RViQuxCxBpYEqzAjEe~8aVxqrAr5v2HSyqx7DKxQSfiS6Adxq9R~PfQK-gcf3PoOfOuE-Gafgx2XkjUpqNspkS1fFBAx-i-PdAg8fn2lDHRAhneB2LKOaQJ3OtraPQssdSufa60x6lzAOxwwRTLemexBIFKb32ff8nGAjsoib8Y0~sbruTrCbZvShO~2VGkLOH7hyoBIPnvA~mhPExl-cBc7wUeYL0eNxqLuX-xLDlgnmPYV3glmqwcMTTvtlUhUk~V1vQofQjazisDWxDe2kq4hDKrIMVekRJHRgh3QIHYoiZos14g__" -b
I'm the second test file
http https://d3cvlqayyqxutd.cloudfront.net/testfile2.txt
HTTP/1.1 403 Forbidden
Connection: keep-alive
Content-Length: 146
Content-Type: text/xml
Date: Thu, 31 Mar 2022 23:50:03 GMT
Server: CloudFront
Via: 1.1 c535ea37f0fd1edbebb6aafb708714a4.cloudfront.net (CloudFront)
X-Amz-Cf-Id: KUqqXOO8jm10svxejOto4ZgBu_jvBrKc1qp4J8IJiZSVkjFFQpyPjQ==
X-Amz-Cf-Pop: ORD56-P2
X-Cache: Error from cloudfront
<?xml version="1.0" encoding="UTF-8"?><Error><Code>MissingKey</Code><Message>Missing Key-Pair-Id query parameter or cookie value</Message></Error>
import { Stack, StackProps, CfnOutput } from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as s3 from "aws-cdk-lib/aws-s3";
import * as cloudfront from "aws-cdk-lib/aws-cloudfront";
import * as origins from "aws-cdk-lib/aws-cloudfront-origins";
import * as apigw from "aws-cdk-lib/aws-apigateway";
import * as lambda from "aws-cdk-lib/aws-lambda";
import * as iam from "aws-cdk-lib/aws-iam";
import * as path from 'path';
export interface PresignedUrlRestApiStackProps extends StackProps {
readonly pubicKey: string;
readonly privateKeySsmPath: string;
}
export class AwsCdkPresignedUrlRestApiStack extends Stack {
constructor(
scope: Construct,id:
string,
props: PresignedUrlRestApiStackProps
) {
super(scope, id, props);
const s3Bkt = new s3.Bucket(this, 'RandoFilesBkt');
const pubKey = new cloudfront.PublicKey(this, 'SignedPubKey', {
encodedKey: props.pubicKey
});
const cfDistribution = new cloudfront.Distribution(this, 'RandoFilesDistribution', {
defaultBehavior: {
origin: new origins.S3Origin(s3Bkt),
trustedKeyGroups: [
new cloudfront.KeyGroup(this, 'SignedKeyGroup', {
items: [pubKey]
})
]
}
});
const api = new apigw.RestApi(this, 'RandoFilesApi', {
description: 'REST API for Random Files in S3',
deployOptions: {
stageName: 'v1'
},
defaultCorsPreflightOptions: {
allowHeaders: [
'X-Api-Key',
'Content-Type',
'Authorization',
'X-Amz-Date'
],
allowOrigins: ['*'],
allowCredentials: true,
allowMethods: ['OPTIONS', 'GET', 'POST']
}
});
const listObjsFn = new lambda.Function(this, 'ListRandoFilesFn', {
runtime: lambda.Runtime.NODEJS_14_X,
handler: 'index.handler',
code: lambda.Code.fromAsset(path.join(__dirname, '..', 'lambdas', 's3-objects')),
environment: {
BUCKET_NAME: s3Bkt.bucketName
}
});
const urlSignerFn = new lambda.Function(this, 'SignUrlFn', {
runtime: lambda.Runtime.NODEJS_14_X,
handler: 'index.handler',
code: lambda.Code.fromAsset(path.join(__dirname, '..', 'lambdas', 'url-signer')),
environment: {
DISTRIBUTION_DOMAIN_NAME: cfDistribution.distributionDomainName,
KEY_ID: pubKey.publicKeyId,
PRIVATE_KEY_SSM_PATH: props.privateKeySsmPath
}
});
urlSignerFn.addToRolePolicy(new iam.PolicyStatement({
actions: ['ssm:GetParameter'],
resources: ['*']
}));
s3Bkt.grantRead(listObjsFn);
const s3ObjectApi = api.root.addResource('s3objects');
s3ObjectApi.addMethod('GET', new apigw.LambdaIntegration(listObjsFn));
const urlsApi = api.root.addResource('urls');
urlsApi.addMethod('POST', new apigw.LambdaIntegration(urlSignerFn));
new CfnOutput(this, 'RestApiUrl', {
value: api.url
});
new CfnOutput(this, 'BucketName', {
value: s3Bkt.bucketName
});
}
}
#!/usr/bin/env node
import 'source-map-support/register';
import * as cdk from 'aws-cdk-lib';
import {
AwsCdkPresignedUrlRestApiStack
} from '../lib/aws-cdk-presigned-url-rest-api-stack';
const app = new cdk.App();
new AwsCdkPresignedUrlRestApiStack(app, 'AwsCdkPresignedUrlRestApiStack', {
privateKeySsmPath: '/tci/demos/cloudfront/signedurls/privatekey',
pubicKey: process.env.PUBLIC_KEY!
});
const AWS = require('aws-sdk');
const ssm = new AWS.SSM();
async function fetchPrvateKey() {
return new Promise(resolve => {
ssm.getParameter({
Name: process.env.PRIVATE_KEY_SSM_PATH,
WithDecryption: true
},
(err, res) => {
if (err) throw err;
resolve(res.Parameter.Value);
})
});
}
exports.handler = async function(event, context) {
console.log(JSON.stringify(event));
const reqPayload = JSON.parse(event.body);
const privateKey = await fetchPrvateKey();
const signer = new AWS.CloudFront.Signer(process.env.KEY_ID, privateKey);
const delta12Hrs = 12 * 60 * 60 * 1000;
const signedUrl = signer.getSignedUrl({
url: `https://${process.env.DISTRIBUTION_DOMAIN_NAME}/${reqPayload.file}`,
expires: Math.floor((Date.now() + delta12Hrs) / 1000)
});
return {
body: JSON.stringify({signedUrl}),
statusCode: 201
};
}