Sharing Private S3 Object With Presigned URLs
by Mike Wzorek
I came across a problem at work the other day where I needed to be able to show a preview of, and allow our users to download, a variety of different types of content from a web app, i.e. video, image, Open Office files, PDF, etc. that are stored in a private S3 bucket
The solution that I found was to generate a pre-signed URL. A pre-signed URL allows you to grant temporary access to users who don’t have permission to directly run AWS operations in your account. A pre-signed URL is signed with your AWS credentials and can be used by any user.
Our API is built in Node.js so I took advantage of the AWS SDK to get the pre-signed URL.
First install the AWS SDK.yarn install aws-sdk
or npm install aws-sdk
.
Next initialize the S3 client in the class constructor.
import AWS, { S3 } from 'aws-sdk';
export class S3Service {
s3: S3;
constructor() {
super();
AWS.config.update({
region: 'us-east-1', // the region your s3 bucket is in.
accessKeyId: process.env.AWS_ACCESS_KEY_ID, // I'm using environment variables to store my secrets
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
});
this.s3 = new S3();
}
// ...
}
There is a few things going on in the implementation, some of which are beyond returning a signed url, but I figured it would be useful to include them. I’ll list them out in order of operations and include the full code snippet towards the end:
- Based on the S3 Object
key
passed into the function I’m grabbing the file type and extension using a lightweight MIME type module, GitHub - broofa/mime: Mime types for JavaScript
import mime from 'mime'
// ...
const type = mime.getType(key); // key = '/my/folder/path/object.pdf'
const extension = mime.getExtension(type || '');
// ...
- Set up the parameters to get the signed url. You will notice a bit of conditional logic in determining the
ResponseContentType
. What’s going on there is that if thecontentType
variable I instantiated in the first step is an image type, i.e. jpeg, png, etc., I want to the pre-signed url header to include the content-type header equal to’binary/octet-stream'
. Returning the image pre-signed URLs like this allows us to show a preview of the image in our web app, but also make them download-able.
const getSignedUrlParams = {
Bucket: s3Bucket, // 'name-of-your-s3-bucket'
Key: key, // key = '/my/folder/path/object.pdf'
Expires: 3600, // time in seconds (3600 = 1 hour). Default is 15 minutes.
ResponseContentType: isImageContentType(contentType as string) ? 'binary/octet-stream' : contentType,
};
- Lastly, I call the
getSignedUrlPromise
function and return the string URL response. I’m using the promise based function because I am making this request in an asynchronous context.
const signedUrl = await this.s3.getSignedUrlPromise(‘getObject’, getSignedUrlParams);
return {
signedUrl,
contentType: type || ‘’,
extension: extension || ‘’,
};
Bringing it all together, here’s the full implementation:
import AWS, { S3 } from 'aws-sdk';
import mime from 'mime';
interface S3SignedUrlResponse {
signedUrl: string;
contentType: string;
extension: string;
}
export class S3Service {
s3: S3;
constructor() {
super();
AWS.config.update({
region: 'us-east-1',
accessKeyId: process.env.AWS_ACCESS_KEY_ID,
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
});
this.s3 = new S3();
}
async getS3ObjectSignedUrl(s3Bucket: string, key: string): Promise<S3SignedUrlResponse> {
try {
const contentType = mime.getType(key); // key = '/my/folder/path/object.pdf'
const extension = mime.getExtension(type || '');
const getSignedUrlParams = {
Bucket: s3Bucket, // 'name-of-your-s3-bucket'
Key: key,
Expires: 3600, // time in seconds (3600 = 1 hour)
ResponseContentType: isImageContentType(contentType as string) ? 'binary/octet-stream' : contentType,
};
const signedUrl = await this.s3.getSignedUrlPromise('getObject', getSignedUrlParams);
return {
signedUrl,
contentType: contentType || '',
extension: extension || '',
};
} catch (error) {
throw new Error('Error getting a signedUrl');
}
}
}
As part of my posts I’m going to make it a point to include some content beyond code. I imagine this will likely take the form of a song I’m enjoying especially while writing code or an insightful article which may or may not be software related.
I hope you got something out this article. Feel free to provide any feedback by contacting me via email or social media.
Song of the Post
Lane 8 - Keep On
Spotify
YouTube
Random Article
As someone who's built enterprise applications in both React and Angular, I do love my frameworks and think they offer a lot of value. That said, with any choice in technology its important to put thought into why you use or need them for your particular use case.
References
AWS Docs - Share Object with Presigned URL
Class: AWS.S3 — AWS SDK for JavaScript