Working with S3 pre-signed URLs

Grant temporary access to objects in AWS S3 buckets without the need to grant explicit permissions.
Shahar Yakov

June 15 2020 · 3 min read

SHARE ON

Working with S3 pre-signed URLs

TL;DR

S3 pre-signed URLs grant temporary access to objects in AWS S3 buckets without the need to grant explicit permissions.

  • Supports only GET and PUT requests.

  • Request headers must exactly match both when creating and using the URLs.

Motivation

It’s a best practice to keep S3 buckets private and only grant public access when absolutely required. Then how can you grant your client access to an object without changing the bucket ACL, creating roles, or providing a user on your account? That’s where S3 pre-signed URLs come in.

Pre-Signed URLs

S3 pre-signed URLs are a form of an S3 URL that temporarily grants restricted access to a single S3 object to perform a single operation — either PUT or GET — for a predefined time limit.

To break it down:

  • It is secure — the URL is signed using an AWS access key

  • It grants restricted access — only one of GET or PUT is allowed for a single URL

  • Only to a single object — each pre-signed URL corresponds to one object

  • With a time-constrained — the URL expires after a set timeout

In the next section, we’ll use these properties to generate an S3 pre-signed URL. So let’s jump over to some code samples.

Key Points

Building a solution with S3 pre-signed URLs is not without its pitfalls. Here are some pointers to save you valuable time:

  1. You must send the same HTTP headers — when accessing a pre-signed URL — as you used when you generated it. For example, if you generate a pre-signed URL with the Content-Type header, then you must also provide this header when you access the pre-signed URL. Beware that some libraries - for example, Axios - attach default headers, such as Content-Type, if you don't provide your own.

  2. The default pre-signed URL expiration time is 15 minutes. Make sure to adjust this value to your specific needs. Security-wise, you should keep it to the minimum possible — eventually, it depends on your design.

  3. To upload a large file — larger than 10MB — you need to use multi-part upload. I’ll cover this topic in my next post.

  4. Pre-signed URLs support only the getObject, putObject and uploadPart functions from the AWS SDK for S3. It's impossible to grant any other access to an object or a bucket, such as listBucket.

  5. Because of the previously mentioned AWS SDK functions limitation, you can’t use pre-signed URLs as Lambda function sources, since Lambda requires both listBucket and getObject access to an S3 object to use as a source.

Implementation

Now that we have all the information we require let’s get to coding.

Each code sample section contains two parts, the producer of the URL and the consumer. I use the JavaScript AWS SDK to generate the pre-signed URLs and the Axios NPM package to access these URLs from the consumer using basic HTTP requests.

In the examples, I use the constants BUCKET_NAME and OBJECT_NAME to represent my bucket and object - replace these with your own as you see fit. Also, there is a placeholder for theaccesskeyId and secretAccessKey, you can read about it here.

Generating S3 pre-signed URLs — The Producer

var AWS = require('AWS')
var cuid = require('cuid')

const s3 = new AWS.S3({
  accessKeyId: /* Bucket owner access key id */,
  secretAccessKey: /* Bucket owner secret */,
  sessionToken: `session-${cuid()}`
})

var params = {
  Bucket: BUCKET_NAME,
  Key: OBJECT_NAME,
  ContentType: 'application/json',
  Expires: 120 // In seconds
}

const readSignedUrl = await s3.getSignedUrlPromise('getObject', params).promise() // Read access
const writeSignedUrl = await s3.getSignedUrlPromise('putObject', params).promise() // Write access

Accessing S3 objects using pre-signed URLs — The Consumer

const axios = require('axios')

// Read object
axios.get(
  readSignedUrl, { // The url which was generated
    headers: {
      'Content-Type': 'application/json'
  },
})

// Write object
axios.put(
  writeSignedUrl, // The url which was generated
  JSON.stringify(JSON_OBJECT), { // Using your own json object
  headers: {
    'Content-Type': 'application/json'
  },
})

Further Reading

By submitting this form, you are accepting our Terms of Service and our Privacy Policy

Thanks for subscribing!

Ready to Get Started?

Request a Demo

Copyright © 2020 Altostra. All rights reserved.