Skip to main content

Signed URL

A signed URL is a time-limited, pre-authorized URL that allows users to access a protected resource (such as an image, video, or document) without exposing the underlying storage system or requiring direct authentication.

These URLs contain a cryptographic signature and optional metadata (like expiration time and access permissions). Only users with a valid signed URL can access the resource.

How it Works​

  • A user (or client) requests access to a protected resource.
  • The server/service generates a signed URL using a private key or a secret.
  • The user can upload, download, or view (can be protected to only download) the resource by using the URL until it expires.
  • Once the URL expires, access is revoked.

Common Use Cases of Signed URLs​

  • Secure File Uploads: Allow clients to upload files directly to storage without exposing backend systems.
  • Secure File Downloads: Allow users to download resources securely without making them public.
  • Access Control: Grant temporary access to internal or confidential resources.

Share AWS S3 Object using Signed URL​

Lets try to share image using signed URL in AWS S3.

Here we will use localstack, a cloud service simulator that works locally in our machine to simulate AWS S3 stack. To install localstack you can follow the guide in here.

Start Localstack​

Start your localstack using optional LOCALSTACK_S3_SKIP_SIGNATURE_VALIDATION=0 to enable validation of S3 pre-signed URL request signature.

➜ LOCALSTACK_S3_SKIP_SIGNATURE_VALIDATION=0 localstack start

__ _______ __ __
/ / ____ _________ _/ / ___// /_____ ______/ /__
/ / / __ \/ ___/ __ `/ /\__ \/ __/ __ `/ ___/ //_/
/ /___/ /_/ / /__/ /_/ / /___/ / /_/ /_/ / /__/ ,<
/_____/\____/\___/\__,_/_//____/\__/\__,_/\___/_/|_|

- LocalStack CLI: 4.2.0
- Profile: default
- App: https://app.localstack.cloud

[13:29:01] starting LocalStack in Docker mode 🐳
───────────────────── LocalStack Runtime Log (press CTRL-C to quit) ─────────────────────

LocalStack version: 4.2.1.dev7
LocalStack build date: 2025-02-28
LocalStack build git hash: 4285fb18c

Ready.

Create Bucket​

Create bucket and add some image to the bucket.

➜ awslocal s3api create-bucket --bucket my-bucket
{
"Location": "/my-bucket"
}
➜ awslocal s3api put-object --bucket my-bucket --key image.png --body bc-logo.png
{
"ETag": "\"0c8c0a48b20dc18c7645de633daf165a\"",
"ChecksumCRC32": "6+7i7g==",
"ChecksumType": "FULL_OBJECT",
"ServerSideEncryption": "AES256"
}

Pre-Sign with AWS CLI​

Now you can create and share a signed URL for your image using this command below.

➜ awslocal s3 presign s3://my-bucket/image.png
http://localhost:4566/my-bucket/image.png?AWSAccessKeyId=test&Signature=XsnJh3uvtdcKlmUHQoVcibO9LD4%3D&Expires=1740985783

Open the URL in your browser and it should start download the image. The URL will expired in 3600s or ypu can override it by adding --expires-in options.

When you try to access the URL after expired it will return an error AccessDenied: Request has expired.

<Error>
<Code>AccessDenied</Code>
<Message>Request has expired</Message>
<RequestId>bf512477-206d-43dc-9bb8-baaee642db8f</RequestId>
<Expires>2025-03-03T07:37:10.000Z</Expires>
<ServerTime>2025-03-03T07:38:04.000Z</ServerTime>
<X-Amz-Expires>300</X-Amz-Expires>
<HostId>9Gjjt1m+cjU4OPvX9O9/8RuvnG41MRb/18Oux2o5H5MY7ISNTlXN+Dz9IG62/ILVxhAGI0qyPfg=</HostId>
</Error>

Pre-Sign with AWS SDK Golang​

Most of the time you will not use the AWS CLI to generate a signed URL. Here are example on how to create a signed URL using AWS SDK connected to localstack in Golang.

package main

import (
"context"
"fmt"
"log"
"time"

"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/credentials"
"github.com/aws/aws-sdk-go-v2/service/s3"
)

func main() {
cfg := aws.Config{
Region: "ap-southeast-1", // any region will work for localstack
Credentials: credentials.NewStaticCredentialsProvider("test", "test", "test"),
}

s3client := s3.NewFromConfig(cfg, func(o *s3.Options) {
o.BaseEndpoint = aws.String("http://localhost:4566")
o.UsePathStyle = true
})

psClient := s3.NewPresignClient(s3client)
req, err := psClient.PresignGetObject(context.TODO(), &s3.GetObjectInput{
Bucket: aws.String("my-bucket"),
Key: aws.String("image.png"),
}, s3.WithPresignExpires(5*time.Minute))
if err != nil {
log.Fatal(err)
}

fmt.Println(req.URL)
}
➜ go run main.go
http://localhost:4566/my-bucket/image.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=test%2F20250303%2Fap-southeast-1%2Fs3%2Faws4_request&X-Amz-Date=20250303T073210Z&X-Amz-Expires=300&X-Amz-Security-Token=test&X-Amz-SignedHeaders=host&x-id=GetObject&X-Amz-Signature=07782ba9ccc584bc5f85fd6b41c19cf79024626ce96830eaf4c12783d23455dd

Other Cloud Providers​

References​