Course outline · 0% complete

0/27 lessons0%

Course overview →

Presigned URLs: temporary access

lesson 4-3 · ~9 min · 12/27

The middle ground

Your app stores each user's invoices in a private bucket. The user clicks Download. Now what? The bucket must stay private, but this one person needs this one file, right now.

Bad option 1: make the bucket public. Lesson 4-2 covered how that ends.

Bad option 2: stream the file through your own server, private bucket → server → user. It works, but every download now costs your server memory and bandwidth, twice.

The S3 answer is a presigned URL. Your server, which does have S3 access through its role (lesson 2-3), uses its credentials to sign a URL that says: whoever holds this exact link may GET this exact object until this exact time. The user's browser then downloads straight from S3. The signature is part of the URL, so S3 can verify it without the user having any AWS identity at all.

browseryour serverhas an IAM roleS3 bucketprivate1. GET /download2. signed URL, expires in 300s3. GET with signature, straight from S3
Presigned flow: the server signs (using its role credentials), the browser fetches directly from S3, and after the expiry time the link is worthless.

The expiry check S3 performs

A presigned URL's expiry check is arithmetic on timestamps in seconds. Here a URL was signed at time 800 with a 300-second lifetime, and a request arrives at time 1000.

now=1000
signed_at=800
expires_in=300
expires_at=$((signed_at + expires_in))
if [ $now -le $expires_at ]; then
  echo "link is VALID ($((expires_at - now))s left)"
else
  echo "link is EXPIRED ($((now - expires_at))s ago)"
fi

Output

link is VALID (100s left)

The link expires at 1100, and the request arrives at 1000, so 100 seconds remain.

S3 can run this check with no help from your application because the expiry is baked into the signature. Changing the timestamp in the URL invalidates the signature, so a holder cannot extend their own access.

A bookmarked presigned link, an hour later

A user bookmarked a link signed at time 800 with a one-hour lifetime and clicks it at time 5000.

now=5000
signed_at=800
expires_in=3600
expires_at=$((signed_at + expires_in))
if [ $now -le $expires_at ]; then
  echo "link is VALID ($((expires_at - now))s left)"
else
  echo "link is EXPIRED ($((now - expires_at))s ago)"
fi

Output

link is EXPIRED (600s ago)

Reading the arithmetic

  • Only now and expires_in change from the previous version. expires_at becomes 4400, and 5000 is past it by 600 seconds.
  • Bookmarking a presigned URL never works for long, and that is the point rather than a limitation. A link that kept working would be a permanent public URL wearing a disguise.
  • The practical design consequence is that apps generate presigned URLs on demand, at the moment the user clicks Download, rather than storing them in a database or emailing them.

Presigned URLs for uploads

An app hands the browser a presigned upload URL so that the upload flows from browser to S3 directly, which keeps large files out of the app server's bandwidth and memory entirely.

The economics match the download case. The server does a tiny signing job, measured in microseconds and bytes, then steps out of the data path. A 2 GB video upload does not occupy your app server at all, so one large upload cannot starve every other request on that machine.

Nothing about the permission model is weakened. The signature still scopes exactly which key may be written and until when, so an attacker holding one upload URL cannot write to a different key or keep using it tomorrow.

The pattern does move one responsibility to the server: deciding the key before the upload happens. That is usually welcome, because it means the application controls the naming scheme rather than trusting a filename supplied by a browser.