Presigned URLs

Generate presigned URLs for upload and download. Expiration settings, security considerations. Code examples in JavaScript, Python, and CLI.

Presigned URLs are temporary, signed URLs that grant time-limited access to upload or download objects without exposing your Access Key ID or Secret Access Key. Use them for direct browser uploads, sharing files with external users, or integrating with clients that cannot securely store credentials.

How Presigned URLs Work

  1. Your server (with credentials) generates a URL that includes a signature and expiration time
  2. You send the URL to the client (browser, mobile app, external system)
  3. The client uses the URL to PUT (upload) or GET (download) directly to storage
  4. After expiration, the URL no longer works
┌─────────────┐    1. Request URL      ┌─────────────┐
│   Client    │ ─────────────────────► │   Server    │
│  (browser)  │                        │ (has keys)  │
└─────────────┘                        └──────┬──────┘
       │                                       │
       │    2. Presigned URL                    │ 3. Generate signed URL
       │ ◄─────────────────────────────────────┘

       │  4. PUT/GET directly to storage

┌─────────────────┐
│  NFYio Storage  │
└─────────────────┘

Generate for Download

JavaScript (AWS SDK v3)

import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
import { GetObjectCommand, S3Client } from '@aws-sdk/client-s3';

const client = new S3Client({
  endpoint: process.env.NFYIO_STORAGE_ENDPOINT,
  region: 'us-east-1',
  credentials: {
    accessKeyId: process.env.AWS_ACCESS_KEY_ID,
    secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
  },
  forcePathStyle: true,
});

const command = new GetObjectCommand({
  Bucket: 'my-bucket',
  Key: 'documents/report.pdf',
});

const url = await getSignedUrl(client, command, { expiresIn: 3600 }); // 1 hour
console.log(url);
// https://storage.yourdomain.com/my-bucket/documents/report.pdf?X-Amz-...

Python (boto3)

import boto3
from botocore.config import Config

s3 = boto3.client(
    's3',
    endpoint_url='https://storage.yourdomain.com',
    config=Config(signature_version='s3v4'),
    region_name='us-east-1',
)

url = s3.generate_presigned_url(
    'get_object',
    Params={'Bucket': 'my-bucket', 'Key': 'documents/report.pdf'},
    ExpiresIn=3600,
)
print(url)

AWS CLI

aws s3 presign s3://my-bucket/documents/report.pdf \
  --expires-in 3600 \
  --endpoint-url https://storage.yourdomain.com

Generate for Upload

JavaScript

import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
import { PutObjectCommand, S3Client } from '@aws-sdk/client-s3';

const command = new PutObjectCommand({
  Bucket: 'my-bucket',
  Key: 'uploads/user-file.pdf',
  ContentType: 'application/pdf',
  // Optional: ContentLength to enforce max size
});

const url = await getSignedUrl(client, command, { expiresIn: 900 }); // 15 min
// Client PUTs the file to this URL with Content-Type: application/pdf

Python

url = s3.generate_presigned_url(
    'put_object',
    Params={
        'Bucket': 'my-bucket',
        'Key': 'uploads/user-file.pdf',
        'ContentType': 'application/pdf',
    },
    ExpiresIn=900,
)

CLI

aws s3 presign s3://my-bucket/uploads/user-file.pdf \
  --expires-in 900 \
  --method put \
  --endpoint-url https://storage.yourdomain.com

Client-Side Usage

Download (Browser)

// Server sends presigned URL to client
const url = await fetch('/api/get-download-url?key=documents/report.pdf').then(r => r.json()).then(d => d.url);

// Client opens or downloads
window.location.href = url;
// Or: <a href={url} download>Download</a>

Upload (Browser)

// Server generates presigned URL
const { url, key } = await fetch('/api/get-upload-url', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ filename: file.name, contentType: file.type }),
}).then(r => r.json());

// Client uploads directly to storage
await fetch(url, {
  method: 'PUT',
  body: file,
  headers: {
    'Content-Type': file.type,
  },
});

Expiration Settings

Use CaseRecommended Expiration
Download link (email, share)1–24 hours (3600–86400)
Upload form5–15 minutes (300–900)
One-time download5–60 minutes
Batch processingMatch job duration

Expiration is encoded in the signature. Extending requires generating a new URL.

Security Considerations

1. Short Expiration for Uploads

Upload URLs should expire quickly (e.g., 15 minutes). The client requests the URL right before upload.

2. Validate Server-Side

Before generating a download URL, verify the user has permission to access the object. The presigned URL bypasses normal auth — anyone with the URL can access it until expiry.

3. Use HTTPS

Always use HTTPS endpoints. Presigned URLs contain a signature but not credentials; HTTPS prevents tampering in transit.

4. Restrict Content-Type (Upload)

Include ContentType in the PutObject params so the client must send that exact type. Reduces risk of uploading unexpected file types.

5. Consider Content-Length

For uploads, you can add ContentLength to enforce max file size. The client must send exactly that many bytes.

6. Don’t Log Full URLs

Presigned URLs are effectively secrets. Avoid logging them in full; truncate or redact in logs.

Custom Response Headers (Download)

Force download with a specific filename:

const command = new GetObjectCommand({
  Bucket: 'my-bucket',
  Key: 'documents/report.pdf',
  ResponseContentDisposition: 'attachment; filename="Report-2026.pdf"',
  ResponseContentType: 'application/pdf',
});

const url = await getSignedUrl(client, command, { expiresIn: 3600 });

Troubleshooting

”SignatureDoesNotMatch”

  • Cause: Clock skew, wrong credentials, or modified URL
  • Fix: Ensure server time is synced (NTP); verify credentials; don’t modify the URL

”AccessDenied” / 403

  • Cause: Key lacks permission, or bucket/object doesn’t exist
  • Fix: Check key scopes (read:objects for download, write:objects for upload); verify bucket and key

CORS Errors (Browser)

  • Cause: Bucket CORS not configured for your origin
  • Fix: Add CORS rules to the bucket — see CORS Configuration

Next Steps