Generating pre-signed URLs in NodeJS

Pre-signed URLs provide a safe way to let users upload to our bucket, or download objects from there. Generating pre-signed URLs is straightforward in the SDK for Node.js. In this post, I'll cover the basics for creating pre-signed URLs.

1. What are pre-signed URLs?

In AWS S3, all objects (files, images etc.) are private by default, which means that only the account (object) owner can access them.

Sometimes it’s necessary to share these objects with external users. It’s definitely not a good idea to give out our security credentials, so AWS came up with the concept of pre-signed URLs.

Pre-signed URLs provide the users of the application with access to the objects in our AWS S3 bucket. Users click on the URL, and they can read (download) the object, or can write (upload) a new object to the bucket, while other objects or buckets remain secure.

The URL expires after a set period of time, which can be set in the params object.

Typical use cases:

  • A service provider allow clients to download the monthly invoices after logging in to their application.
  • Users can upload photos and show them to everyone they are connected to.

2. Pre-requisites

To create a pre-signed URL, one needs to have an AWS account and credentials for programmatic access to make the following code work.

3. The getSignedUrl method

Both the up- and download operations can be done using the getSignedUrl method of the SDK for Node.js.

The method accepts the name of the operation method as a string and the parameters of the operation method.

For example, for a download URL we need to specify the getObject method as a string and add the parameters of this method. Normally, putObject can be used to upload objects to S3, so this method needs to be specified with the relevant parameters when we want to create an upload URL.

More information on up- and downloading objects from S3 can be found in this post.

As such, the code for both situations will be very similar.

The generation of the pre-signed URL occurs locally, and the account owner (we) will sign it using our credentials, more accurately, the secret access key, which acts as a private key. As a result, the getSignedUrl method doesn’t have the option to chain the promise method to it.

3.1. Set up the project

Create a folder with a pleasant name, and run npm init -y. The next step is to install the SDK by running npm install aws-sdk.

We’ll work with the Architecting for the Cloud - AWS Best Practices document, so let’s upload it first to S3 either through the console, or following the steps in the last post.

The file needs to be saved in a bucket of a unique name, so the name of the bucket in your example should be different from s3-upload-and-download-suad123.

3.2. Generate a pre-signed URL for download

In a file called getObject-pre-signed-url.js, we can have the following code:

const AWS = require('aws-sdk')

const s3 = new AWS.S3({
  region: 'us-west-2',
  signatureVersion: 'v4',
})

const BUCKET_NAME = 's3-upload-and-download-suad123'

const getSignedUrlForDownload = async () => {
  const params = {
    Bucket: BUCKET_NAME,
    Key: 'AWS_Cloud_Best_Practices.pdf',
    Expires: 60,
  }

  const url = await new Promise((resolve, reject) => {
    s3.getSignedUrl('getObject', params, (err, url) => {
      if (err) reject(err)

      resolve(url)
    })
  })

  return url
}

getSignedUrlForDownload()
  .then((url) => {
    console.log(url)
  }).catch((e) => {
    console.log(e)
  })

We specifiy the region and signatureVersion at a service level.

The getSignedUrlForDownload function contains the parameters for the getObject method we use for downloading objects from S3.

There’s one additional key in the params object, which is specific to getSignedUrl. The Expires key specifies the validity of the link in seconds. In our case, the download link is valid for 1 minute.

As it was stated above, it’s not possible to attach the promise built-in AWS method to getSignedUrl. Because it accepts a callback as the third argument, we can wrap getSignedUrl in a Promise.

When an error occurs, the promise is rejected with the error. On the happy side, if everything goes well, we resolve the promise with the URL.

Because we work with a promise here, we can await it inside getSignedUrlForDownload, but it needs to be an async function.

This way, getSignedUrlForDownload will return a promise (every async function returns a promise), so we can call it with a then and catch.

3.3. The URL

In the terminal, run node with the name of the file, i.e. node getObject-pre-signed-url.js, and the URL should be seen in the console:

https://s3-upload-and-download-suad123.s3.us-west-2.amazonaws.com/AWS_Cloud_Best_Practices.pdf?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=YOUR_ACCESS_KEY_us-west-2%2Fs3%2Faws4_request&X-Amz-Date=20190527T205311Z&X-Amz-Expires=60&X-Amz-Signature=4eecbf02f9ff51b9a84144fa26751aa3b7c891058f5532533440b3a080bbd0ee&X-Amz-SignedHeaders=host

As it can be seen, the URL contains the name of the bucket and the region with the name of the object we want to download.

The URL also comes with some interesting characters as query parameters. It has the encryption algorithm (SHA-256), the access key of the account owner, the date, the expiration of the link and the signature of the owner of the object.

The upload is very similar to the download, the difference is that we need to use the putObject method inside getSignedUrl:

const getSignedUrlForUpload = async () => {
  const params = {
    Bucket: BUCKET_NAME,
    Key: 'AWS_Cloud_Best_Practices.pdf',
    Expires: 60,
    ContentType: 'application/pdf',
  }


  const url = await new Promise((resolve, reject) => {
    s3.getSignedUrl('putObject', params, (err, url) => {
      if (err) reject(err)

      resolve(url)
    })
  })

  return url
}

uploadFileToS3()
  .then((res) => {
    console.log(res)
  }).catch((e) => {
    console.log(e)
  })

Here we also specify the ContentType key in the params object, which is application/pdf.

Calling the method will result in the URL, which can then be pasted in Postman, for example. Ensure that you choose the PUT method, and the body (the file to upload) should be in binary format.

Depending on the size, the file should be up in S3 within a few seconds.

4. Conclusion

Objects in S3 are private by default. In some cases, access need to be provided to these objects either in a form of a download or upload.

Pre-signed URLs provide a safe way to access objects stored in S3. For both upload and download, we use the getSignedUrl method, which generates a one-time-only link to the given resource.

Thanks for reading, and see you next time.

How to generate presigned URLs using a Lambda function