AWS S3 Pre-Signed URLs Explained: Secure File Sharing with Node.js

Learn how to use AWS S3 pre-signed URLs to securely share files from a Node.js app. Pre-signed URLs enable temporary, controlled access to files in your S3 bucket without exposing them publicly. Perfect for secure file sharing, this guide walks through setup, code, and best practices.

AWS S3 Pre-Signed URLs Explained: Secure File Sharing with Node.js

AWS S3 (Simple Storage Service) is a popular choice for securely storing files in the cloud. However, in many scenarios, you might want to grant temporary access to files in your S3 bucket without exposing them publicly. For this, AWS offers a powerful feature called pre-signed URLs. In this guide, we’ll learn how to use pre-signed URLs to securely share files stored in S3 using Node.js.

If you're new to uploading files to S3, you might want to start with my previous blog post on setting up file uploads to S3 using Node.js and Express. This post will build on that foundation, adding secure, temporary access to those files via pre-signed URLs.

Upload Files to AWS S3: Implement in Node.js + Express Server
Learn how to set up a Node.js + Express server to upload files securely to Amazon S3. This guide covers configuring AWS SDK, using multer for file handling, and setting up environment variables. With scalable, durable storage in S3, your app can handle file uploads efficiently and securely.

What Are AWS S3 Pre-Signed URLs?

A pre-signed URL is a temporary, unique URL that grants time-limited access to a specific file in an S3 bucket. Pre-signed URLs are ideal for cases where you need to share a file temporarily or restrict access without exposing the entire bucket to the public.

Pre-signed URLs are generated by the server and can be set to expire within a specified time, such as 60 seconds, 10 minutes, or several hours. The URL allows authorized access to the object for anyone with the link until it expires.

Prerequisites

To follow along, make sure you have:

  1. Node.js and npm installed.
  2. An AWS S3 bucket and AWS credentials set up with the necessary permissions.
  3. Basic knowledge of Express and file uploads with AWS S3.

Step 1: Set Up the S3 Client

To work with S3, we’ll use AWS SDK’s S3Client and set up our connection parameters. Create a s3Client.js file to initialize the S3 client:

import { S3Client } from "@aws-sdk/client-s3";
import dotenv from "dotenv";

dotenv.config();

export const s3 = new S3Client({
  region: process.env.AWS_REGION,
  credentials: {
    accessKeyId: process.env.AWS_ACCESS_KEY_ID,
    secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
  },
});

Step 2: Set Up File Upload Handling with Multer

In order to upload and manage files in memory, we use multer, a Node.js middleware. Here’s a quick setup for using multer with memory storage:

import multer from "multer";

const storage = multer.memoryStorage();
export const upload = multer({ storage });

Now, files will be stored in memory, allowing us to upload them directly to S3 or generate pre-signed URLs without saving them on the server.

Step 3: Generate a Pre-Signed URL for Secure File Access

Our main focus is generating a pre-signed URL to allow secure, temporary access to a file stored in S3. Let’s create a controller function that generates a pre-signed URL for an uploaded file.

Here’s how the getPresignedAttachmentUrl function works:

  • We fetch the attachment details from our database based on attachmentId provided in the request.
  • We verify if the requesting user has access to the file.
  • We extract the file key from the S3 URL and use the getSignedUrl function to generate a secure, time-limited URL.
import { GetObjectCommand } from "@aws-sdk/client-s3";
import { s3 } from "../s3Client.js";
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
import { AttachmentModel } from "./model";
import SharingModel from "../shared_items/model";
import { NoteModel } from "../notes/model";

export const getPresignedAttachmentUrl = async (req, res) => {
  const { attachmentId } = req.params;
  const userId = req.user.userId;

  try {
    const attachment = await AttachmentModel.findById(attachmentId);
    if (!attachment) {
      return res.status(404).json({ message: "Attachment not found" });
    }

    // Check if user has access to the attachment
    const note = await NoteModel.findOne({ user: userId, attachments: attachmentId });
    const sharingRecord = await SharingModel.findOne({ sharedWithIds: userId });

    if (!note && !sharingRecord) {
      return res.status(403).json({ message: "Access denied" });
    }

    // Extract file key from S3 URL
    const fileKey = attachment.s3Url.split(".com/")[1];

    // Generate pre-signed URL
    const command = new GetObjectCommand({
      Bucket: process.env.AWS_BUCKET_NAME,
      Key: fileKey,
    });

    const signedUrl = await getSignedUrl(s3, command, { expiresIn: 60 });

    res.status(200).json({ fileUrl: signedUrl });
  } catch (error) {
    console.error("Error generating pre-signed URL:", error);
    res.status(500).json({ message: "Error generating pre-signed URL", error });
  }
};

Explanation of the Code

  1. Attachment Verification: We first check if the file exists in our database. If not, a 404 Not Found error is returned.
  2. Access Control: We verify if the requesting user is allowed access to the file by checking permissions in NoteModel and SharingModel.
  3. Generating the Pre-Signed URL:
    • We create a GetObjectCommand using the S3 bucket name and file key.
    • Using getSignedUrl, we create a pre-signed URL with a 60-second expiration.
  4. Sending the Pre-Signed URL: If the URL is successfully generated, it is returned in the response, allowing the user to download the file securely for the next 60 seconds.

Step 4: Setting Up the Express Route

Next, let’s set up an Express route to handle requests for pre-signed URLs. In your main server file (index.js), create the route, and link it to the getPresignedAttachmentUrl controller.

import express from "express";
import { getPresignedAttachmentUrl } from "./controllers/presignedUrlController.js";

const app = express();
const PORT = process.env.PORT || 3000;

// Route to get a pre-signed URL for file access
app.get("/attachments/:attachmentId/url", getPresignedAttachmentUrl);

app.listen(PORT, () => {
  console.log(`Server running on port ${PORT}`);
});

Testing the Pre-Signed URL Endpoint

To test this endpoint:

  1. Start the server: node index.js.
  2. Send a GET request to http://localhost:3000/attachments/{attachmentId}/url with a valid attachmentId.
  3. You should receive a JSON response containing a fileUrl, which is the pre-signed URL for accessing the file.

If the URL is successfully generated, you’ll receive a response like this:

{
  "fileUrl": "https://your-bucket.s3.your-region.amazonaws.com/your-file-key?X-Amz-Expires=60&X-Amz-Signature=abc123..."
}

Clicking or navigating to this fileUrl in a browser will allow access to the file for 60 seconds.

Benefits of Using S3 Pre-Signed URLs

  1. Security: Pre-signed URLs allow you to grant time-limited access to files without exposing the entire bucket.
  2. Flexibility: You can control the expiration time and permissions, enabling you to create custom file-sharing options.
  3. Reduced Bandwidth Costs: Since files are accessed directly from S3, you don’t need to proxy the file through your server.

Key Considerations for Pre-Signed URLs

  • Expiration Time: Choose an expiration time that balances security with user experience.
  • Access Control: Make sure only authorized users can generate pre-signed URLs.
  • Permissions: Pre-signed URLs can be generated for uploading, downloading, or other S3 operations, so set appropriate permissions based on your use case.

Pre-signed URLs are a powerful feature for securely sharing files from AWS S3. By generating URLs that expire after a certain time, you can grant temporary access to your files while keeping your S3 bucket private.

With both uploading and secure access in place, your application can handle file management efficiently and securely.

That's it for this article! See ya 👋