Using AWS S3 Signed URLs for Secure File Uploads
Introduction
AWS S3 presigned URLs allow clients to upload files directly to S3 without exposing AWS credentials or granting broad permissions.
Prerequisites
- AWS SDK v3 for JavaScript
- S3 bucket with appropriate CORS config
Step 1: Configure AWS SDK
npm install @aws-sdk/client-s3 @aws-sdk/s3-request-presigner
Create lib/s3.ts
:
import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3';
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
const s3 = new S3Client({ region: process.env.AWS_REGION });
const bucketName = process.env.S3_BUCKET_NAME!;
export async function generateUploadURL(key: string): Promise<string> {
const command = new PutObjectCommand({
Bucket: bucketName,
Key: key,
ContentType: 'application/octet-stream',
});
const signedUrl = await getSignedUrl(s3, command, { expiresIn: 3600 }); // 1 hour
return signedUrl;
}
Step 2: Create API Route
Create pages/api/upload-url.ts
(or app/api/upload-url/route.ts
):
import { NextRequest, NextResponse } from 'next/server';
import { generateUploadURL } from '@/lib/s3';
export async function GET(req: NextRequest) {
const { searchParams } = new URL(req.url);
const filename = searchParams.get('filename');
if (!filename) {
return NextResponse.json({ error: 'Filename is required' }, { status: 400 });
}
const key = `uploads/${Date.now()}-${filename}`;
const url = await generateUploadURL(key);
return NextResponse.json({ url, key });
}
Step 3: Client-Side Upload
async function uploadFile(file) {
// Get signed URL
const res = await fetch(`/api/upload-url?filename=${file.name}`);
const { url, key } = await res.json();
// Upload file
await fetch(url, {
method: 'PUT',
headers: { 'Content-Type': file.type },
body: file,
});
return key;
}
Step 4: Configure S3 CORS
<CORSConfiguration>
<CORSRule>
<AllowedOrigin>*</AllowedOrigin>
<AllowedMethod>PUT</AllowedMethod>
<AllowedHeader>*</AllowedHeader>
</CORSRule>
</CORSConfiguration>
Summary
S3 presigned URLs provide a secure method for direct client uploads, minimizing backend load and eliminating the need to expose AWS credentials.