Secure Uploads to AWS S3 with Ruby on Rails

Do not expose your secrets on the client. The following guide will outline how to upload files directly to S3 without exposing your secret tokens.

Image for post
Image for post
Photo by Philipp Katzenberger on Unsplash

Introduction

A direct upload to the storage server (S3, GCP storage, etc) may be desired when working with large files. Since uploading through the server would consume resources with the handling of these long running requests.

Image for post
Image for post
Image for post
Image for post

Example with Code

Pre-requisites

You will require the official aws-sdk-s3 gem to be installed and configured with your credentials and region.

Step 1: Method for Generating Signed URL

In your model (ex: models/attachment.rb) add a class method for generating a signed URL for S3 upload.

def self.generate_upload_url(file_name)
Aws::S3::Presigner.new.presigner.presigned_url(
:put_object,
bucket: ENV['MY_S3_BUCKET_NAME'],
key: file_name,
use_accelerate_endpoint: true,
expires_in: 300 # Number of seconds the URL is valid for
)
end

Step 2: Add Controller for Generating a Signed URL

We will create a POST action in our controller that will receive a param called file_name and return a signed URL that allows the upload of this specific file. Note: Instead of using the file_name directly the controller could return an upload file name to be used such as a hash or UUID.

# POST api/attachments/generate_upload_url
def generate_upload_url
ensure_user_is_authorized_to_perform_this_action
@signed_url = Attachment.generate_upload_url(params[:file_name])
render json: { url: @signed_url }
end

Step 3: Add Controller for Receiving File Upload Notice

This is the step that one would usually start with in the insecure approach. In this POST action, the controller will receive the relevant information on the uploaded attachment and persist the model. Note: An additional step could be added to verify that the uploaded file actually exists in S3.

# POST api/attachments
def create
ensure_user_is_authorized_to_perform_this_action
@attachment = Attachment.new(safe_params) if @attachment.save
render json: @attachment
else
render json: @attachment.errors, status: :unprocessable_entity
end
end

Conclusion

The additional step of generating a pre-signed upload URL removes the need that AWS credentials be publicly available on the client for direct uploads.

Written by

A curious minded engineer.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store