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