Luciano Mammino (@loige)
AWS User Group: Dublin Meetup / 2022-11-17
META_SLIDE!
WTF?
👋 I'm Luciano (🇮🇹🍕🍝🤌)
👨💻 Senior Architect @ fourTheorem (Dublin 🇮🇪)
📔 Co-Author of Node.js Design Patterns 👉
Accelerated Serverless | AI as a Service | Platform Modernisation
✉️ Reach out to us at hello@fourTheorem.com
😇 We are always looking for talent: fth.link/careers
We host a weekly podcast about AWS
POST /profilepic/upload HTTP/1.1 Host: api.meower.com Content-Type: text/plain Content-Length: 9 Some data
Method
Path
Version
Headers
Body
PUT /profilepic/upload HTTP/1.1 Host: api.meower.com Content-Type: image/jpeg Content-Length: 2097852 ����JFIFHH������"�� ���Dl��FW�'6N�()H�'p��FD3 [...]
read 2097852 bytes
Lambda proxy integration*
* JSON Based protocol mapping to HTTP (JSON Request / JSON Response)
{
"resource": "/profilepic/upload",
"path": "/profilepic/upload",
"httpMethod": "POST",
"headers": {
"Content-Type": "text/plain",
"Content-Length": "9",
"Host": "api.meower.com",
},
"body": "Some data"
}
{
"resource": "/profilepic/upload",
"path": "/profilepic/upload",
"httpMethod": "PUT",
"headers": {
"Content-Type": "image/jpeg",
"Content-Length": "2097852",
"Host": "api.meower.com",
},
"body": "????????"
}
{
"resource": "/profilepic/upload",
"path": "/profilepic/upload",
"httpMethod": "PUT",
"headers": {
"Content-Type": "image/jpeg",
"Content-Length": "2097852",
"Host": "api.meower.com",
},
"isBase64Encoded": true,
"body": "/9j/4AAQSkZJRgABAQEASABIAAD/2w[...]"
}
1. Parse request (JSON)
2. Decode body (Base64)
3. Validation / resize
4. ...
Lambda proxy integration request
/profilepic/upload
1. Parse request (JSON)
2. Decode body (Base64)
3. Validation / resize
4. ...
Lambda proxy integration request
/profilepic/upload
😎
Upload: 6 MB / 30 sec
... not really!
What about supporting big images or even videos?
✅ Long lived connection
✅ No size limit
An S3 built-in feature
to authorize operations (download, upload, etc) on a bucket / object
using time-limited authenticated URLs
* yeah, this can be a Lambda as well 😇
*
* yeah, this can be a Lambda as well 😇
*
* yeah, this can be a Lambda as well 😇
*
* yeah, this can be a Lambda as well 😇
*
✅
* yeah, this can be a Lambda as well 😇
*
✅
I lied to you a little in those diagrams... 🤥
It's a decent mental model, but it's not accurate 😅
The server never really talks with S3!
The server actually creates the signed URL by itself!
We will see later what's the security model around this idea!
✅ It's a managed feature (a.k.a. no servers to manage)
✅ We can upload and download arbitrarily big files with no practical limits*
✅ Reasonably simple and secure
👍 Seems good to me!
* objects in S3 are "limited" to 5TB (when using multi-part upload), 5 GB otherwise.
$ aws s3 presign \
s3://finance-department-bucket/2022/tax-certificate.pdf
https://s3.amazonaws.com/finance-department-bucket/2022/tax-certificate.pdf?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIA3SGQVQG7FGA6KKA6%2F20221104%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20221104T140227Z&X-Amz-Expires=3600&X-Amz-SignedHeaders=host&X-Amz-Signature=b228dbec8c1008c80c162e1210e4503dceead1e4d4751b4d9787314fd6da4d55
Whoever has this URL can download the tax certificate!
https://s3.amazonaws.com/finance-department-bucket/2022/tax-certificate.pdf?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIA3SGQVQG7FGA6KKA6%2F20221104%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20221104T140227Z&X-Amz-Expires=3600&X-Amz-SignedHeaders=host&X-Amz-Signature=b228dbec8c1008c80c162e1210e4503dceead1e4d4751b4d9787314fd6da4d55
https://s3.amazonaws.com
/finance-department-bucket
/2022/tax-certificate.pdf
?X-Amz-Algorithm=AWS4-HMAC-SHA256
&X-Amz-Credential=AKIA3SGQXQG7XXXYKKA6%2F20221104...
&X-Amz-Date=20221104T140227Z
&X-Amz-Expires=3600
&X-Amz-SignedHeaders=host
&X-Amz-Signature=b228dbec8c1008c80c162e1210e4503dceead1e4d4...
What if I change this to /passwords.txt?
👿
https://s3.amazonaws.com
/finance-department-bucket
/2022/tax-certificate.pdf
?X-Amz-Algorithm=AWS4-HMAC-SHA256
&X-Amz-Credential=AKIA3SGQXQG7XXXYKKA6%2F20221104...
&X-Amz-Date=20221104T140227Z
&X-Amz-Expires=3600
&X-Amz-SignedHeaders=host
&X-Amz-Signature=b228dbec8c1008c80c162e1210e4503dceead1e4d4...
Are these credentials still valid?
Is the URL expired?
Does the signature match the data in the request?
Photo by CHUTTERSNAP on Unsplash
⚠️ Also note that you can use a pre-signed URL as many times as you want until it expires
Anyone with valid credentials can create a pre-signed URL (client side)
valid credentials = Role, User, or Security Token
The generated URL inherits the permissions of the credentials used to generate it
This means you can generate pre-signed URLs for things you don't have access to 😅
$ aws s3 presign s3://ireland/i-love-you
https://ireland.s3.eu-west-1.amazonaws.com/i-love-you?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIA3ABCVQG7FGA6KKA6%2F20221115%2Feu-west-1%2Fs3%2Faws4_request&X-Amz-Date=20221115T182036Z&X-Amz-Expires=3600&X-Amz-SignedHeaders=host&X-Amz-Signature=75749c92d94d03e411e7bbf64419f2af09301d1791b0df54c639137c715f7888
😱
I swear I don't even know if this bucket exists or who owns it!
AWS SDK for JavaScript v3
import { S3Client, GetObjectCommand } from '@aws-sdk/client-s3'
import { getSignedUrl } from '@aws-sdk/s3-request-presigner'
const s3Client = new S3Client()
const command = new GetObjectCommand({
Bucket: "some-bucket",
Key: "some-object"
})
const preSignedUrl = await getSignedUrl(s3Client, command, {
expiresIn: 3600
})
console.log(preSignedUrl)
PUT <preSignedURL> HTTP/1.1 Host: <bucket>.s3.<region>.amazonaws.com Content-Length: 2097852 ����JFIFHH������"�� ���Dl��FW�'6N�()H�'p��FD3 [...]
import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3'
import { getSignedUrl } from '@aws-sdk/s3-request-presigner'
const s3Client = new S3Client()
const command = new PutObjectCommand({
Bucket: "some-bucket",
Key: "some-object"
})
const preSignedUrl = await getSignedUrl(s3Client, command, {
expiresIn: 3600
})
console.log(preSignedUrl)
Only difference with the previous example
You cannot set a limit on the upload size (max of 5 GB)! *
You can limit the Content-Type but you can specify exactly one
* Unless you know the exact size in advance
It uses the multipart/form-data encoding (form upload)
Gives more freedom to the client to shape the request (Content-Type, file name, etc)
It uses a policy mechanism to define the "rules" of what can be uploaded
E.g. you can limit the supported mime types and provide a maximum file size
You can use it to upload from a web form and even configure the redirect URL
It's not really a URL but more of a pre-signed form!
POST / HTTP/1.1 Host: <bucket>.s3.amazonaws.com Content-Type: multipart/form-data; boundary=9431149156168 Content-Length: 2097852 --9431149156168 Content-Disposition: form-data; name="key" picture.jpg --9431149156168 Content-Disposition: form-data; name="X-Amz-Credential" AKIA3SGABCDXXXA6KKA6/20221115/eu-west-1/s3/aws4_request --9431149156168 Content-Disposition: form-data; name="Policy" eyJleHBpcmF0aW9uIjoiMjAyMi0xMS0xNVQyMDo0NjozN1oiLCJjb25kaXRpb25zIjpbWyJj[...] --9431149156168 Content-Disposition: form-data; name="X-Amz-Signature" 2c1da0001dfec7caea1c9fb80c7bc8847f515a9e4483d2942464f48d2f827de7 --9431149156168 Content-Disposition: form-data; name="file"; filename="MyFilename.jpg" Content-Type: image/jpeg ����JFIFHH������"�� ���Dl��FW�'6N�()H�'p��FD3[...] --9431149156168--
A JSON object (Base64 encoded) that defines the upload rules (conditions) and the expiration date
This is what gets signed: you cannot alter the policy without breaking the signature
{
"expiration": "2022-11-15T20:46:37Z",
"conditions": [
["content-length-range", 0, 5242880],
["starts-with", "$Content-Type", "image/"],
{"bucket": "somebucket"},
{"X-Amz-Algorithm": "AWS4-HMAC-SHA256"},
{"X-Amz-Credential": "AKIA3SGABCDXXXA6KKA6/20221115/eu-west-1/s3/aws4_request"},
{"X-Amz-Date": "20221115T194637Z"},
{"key": "picture.jpg"}
]
}
import { S3Client } from '@aws-sdk/client-s3'
import { createPresignedPost } from '@aws-sdk/s3-presigned-post'
const { BUCKET_NAME, OBJECT_KEY } = process.env
const s3Client = new S3Client()
const { url, fields } = await createPresignedPost(s3Client, {
Bucket: 'somebucket',
Key: 'someobject',
Conditions: [
['content-length-range', 0, 5 * 1024 * 1024] // 5 MB max
],
Fields: {
success_action_status: '201',
'Content-Type': 'image/png'
},
Expires: 3600
})
console.log({ url, fields })
// you can use `url` and `fields` to generate an HTML form
const code = `<h1>Upload an image to S3</h1>
<form action="${url}" method="post" enctype="multipart/form-data">
${Object.entries(fields).map(([key, value]) => {
return `<input type="hidden" name="${key}" value="${value.replace(/"/g, '"')}">`
}).join('\n')}
<div><input type="file" name="file" accept="image/png"></div>
<div><input type="submit" value="Upload"></div>
</form>`
It supports only 1 file (cannot upload multiple files in one go)
The file field must be the last entry in the form
(S3 will ignore every other field after the file)
From the browser (AJAX) you need to enable CORS on the bucket
PUT is simpler but definitely more limited
POST is slightly more complicated (and less adopted) but it's more flexible
You should probably put some time into learning POST and use that!
S3 pre-signed URLs are not limited to GET, PUT or POST operations
You can literally create pre-signed URLs for any command
(DeleteObject, ListBuckets, MultiPartUpload, etc...)
S3 pre-signed URLs are a great way to authorise operations on S3
They are generally used to implement upload/download features
The signature is created client-side so you can sign anything. Access is validated at request time
This is not the only solution, you can also use the JavaScript SDK from the frontend and get limited credentials from Cognito (Amplify makes that process simpler)
For upload you can use PUT and POST, but POST is much more flexible
💬 PS: Meower.com doesn't really exist... but... do you want to invest?! It's a great idea, trust me!
Cover photo by Kelly Sikkema on Unsplash
THANKS! 🙌